﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Xml;
using DimensionsHelper.Common.Contracts.Metadata;
using DimensionsHelper.Common.Utils;

namespace DimensionsHelper.Services.Metadata;

public class MetadataSerializer(MetadataSerializeOptions options) : IMetadataSerializer
{
    private const int LowerInfinity = -2147483648;
    private const int UpperInfinity = 2147483647;


    public MetadataSerializer() : this(new MetadataSerializeOptions())
    {
    }


    public MetadataSerializeOptions Options { get; set; } = options;

    private static bool GetBooleanValue(string? value)
    {
        return value is "-1" or "1";
    }


    private static void TrySetEnumValue<T>(string? value, Action<T> callback)
    {
        if (string.IsNullOrEmpty(value))
        {
            return;
        }

        if (int.TryParse(value, out var enumValue))
        {
            callback((T)Enum.ToObject(typeof(T), enumValue));
        }
    }

    private static void TrySetIntValue(string? value, Action<int> callback)
    {
        if (string.IsNullOrEmpty(value))
        {
            return;
        }

        if (int.TryParse(value, out var intValue))
        {
            callback(intValue);
        }
    }

    private static void TrySetDoubleValue(string? value, Action<double> callback)
    {
        if (string.IsNullOrEmpty(value))
        {
            return;
        }

        if (double.TryParse(value, out var dbValue))
        {
            callback(dbValue);
        }
    }


    private static void JoinFieldNames(List<string> targetList, IField field, bool level, string? parentName)
    {
        var fullName = (parentName ?? "") + field.Name;
        targetList.Add(fullName);

        switch (field)
        {
            case IBlock block:
                block.Fields.ForEach(f => JoinFieldNames(targetList, f, level, $"{fullName}."));
                break;

            case IArray array:
                {
                    if (!level)
                    {
                        array.Class.Fields.ForEach(f =>
                            JoinFieldNames(targetList, f, true, $"{fullName}."));
                        break;
                    }

                    if (array is
                        {
                            IteratorType: IteratorTypes.NumericRanges,
                            Ranges: { LowerBound: int lb, UpperBound: int rb }
                        })
                    {
                        for (var i = lb; i <= rb; i++)
                        {
                            var index = i;
                            array.Class.Fields.ForEach(f =>
                                JoinFieldNames(targetList, f, true, $"{fullName}[{index}]."));
                        }
                    }
                    else if (array.IteratorType == IteratorTypes.Categorical)
                    {
                        foreach (var cate in array.Categories)
                        {
                            array.Class.Fields.ForEach(f =>
                                JoinFieldNames(targetList, f, true, $"{fullName}[{{{cate.Name}}}]."));
                        }
                    }
                    else
                    {
                        array.Class.Fields.ForEach(f =>
                            JoinFieldNames(targetList, f, true, fullName + "."));
                    }

                    break;
                }
        }
    }


    public static string[] GetFieldList(IMetadataDocument document, bool level)
    {
        var fieldNames = new List<string>();

        foreach (var field in document.Fields)
        {
            JoinFieldNames(fieldNames, field, level, null);
        }

        return fieldNames.ToArray();
    }


    #region Xml Deserialize Methods

    /// <summary>
    /// XML文档中，在IArray或者IBlock内定义的分类列表，通过引用放置在下级types节点内，原始的定义放置在IMetadataDocument.Types列表中。
    /// 在引用列表时，使用的对象UUID是定义在IMetadataDocument.Types的UUID，在引用时无法直接查询到正确的LevelDepth值。
    /// 因此，需要在整个文档反序列化完成后，更新列表定义的LevelDepth。
    /// </summary>
    private static readonly List<Action<IMetadataDocument>> CallbacksToCorrectElementListDepth = new();

    public void Deserialize(string fileName, IMetadataDocument document)
    {
        CallbacksToCorrectElementListDepth.Clear();

        var xmlDom = new XmlDocument();
        xmlDom.Load(fileName);

        var namespaceManager = new XmlNamespaceManager(xmlDom.NameTable);
        namespaceManager.AddNamespace("mdm", MetadataDocument.MdmXmlNamespace);

        var root = xmlDom.SelectSingleNode("xml/mdm:metadata", namespaceManager) ??
                   throw new($"文件'{fileName}'不是有效的XML文档。");

        if (document is not MetadataDocument metadataDom)
        {
            throw new ArgumentException("不支持的类型。", nameof(document));
        }

        DeserializeMetadataDocumentInfo(root, metadataDom);

        XmlNode? designNode = null;

        foreach (XmlNode child in root.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "categorymap":
                    DeserializeCategoryMap(child, (CategoryMap)metadataDom.CategoryMap);
                    break;

                case "atoms":
                    DeserializeAtoms(child, metadataDom.Atoms);
                    break;

                case "properties":
                    DeserializeProperties(child, metadataDom.Properties);
                    break;

                case "templates":
                    DeserializeTemplates(child, (Templates)metadataDom.Templates);
                    break;

                case "labels":
                    DeserializeLabels(child, (MetadataLabels)metadataDom.Label);
                    break;

                case "styles":
                    DeserializeStyles(child,
                        (Styles)(metadataDom.Style ??= new Styles(metadataDom, metadataDom)));
                    break;

                case "labelstyles":
                    DeserializeStyles(child,
                        (Styles)(metadataDom.LabelStyle ??= new Styles(metadataDom, metadataDom)));
                    break;

                case "savelogs":
                    DeserializeSaveLog(child, (SaveLogs)metadataDom.SaveLogs);
                    break;

                case "contexts":
                    DeserializeContexts<IContext, Context>(child, (Contexts)metadataDom.Contexts,
                        DeserializeContext, "context");
                    break;

                case "scripttypes":
                    DeserializeContexts<IContext, Context>(child, (Contexts)metadataDom.ScriptContexts,
                        DeserializeContext, "context");
                    break;

                case "labeltypes":
                    DeserializeContexts<IContext, Context>(child, (Contexts)metadataDom.LabelTypes,
                        DeserializeContext, "context");
                    break;

                case "routingcontexts":
                    DeserializeContexts<IContext, Context>(child, (Contexts)metadataDom.RoutingContexts,
                        DeserializeContext, "context");
                    break;

                case "languages":
                    DeserializeContexts<ILanguage, Language>(child, (Languages)metadataDom.Languages,
                        DeserializeLanguage, "language");
                    break;

                case "datasources":
                    DeserializeDataSources(child, (DataSources)metadataDom.DataSources);
                    break;

                case "mappings":
                    DeserializeVariableInstanceMap(child, (VariableInstances)metadataDom.Mapping);
                    break;

                case "systemrouting":
                    DeserializeRoutings(child, (Routings)metadataDom.Routings);
                    break;

                case "versionlist":
                    DeserializeVersionList(child, (VersionList)metadataDom.VersionList);
                    break;

                case "definition":
                    foreach (XmlNode defChild in child.ChildNodes)
                    {
                        switch (defChild.Name.ToLower())
                        {
                            case "categories":
                                DeserializeElementList(defChild, (MetadataTypes)metadataDom.Types, 0);
                                break;

                            case "othervariable":
                            case "multiplier-variable":
                            case "variable":
                                DeserializeVariable(defChild, (Variables)metadataDom.Variables);
                                break;

                            case "page":
                                DeserializePage(defChild, (Pages)metadataDom.Pages);
                                break;
                        }
                    }

                    break;

                case "system":
                    DeserializeFields(child, (Fields)metadataDom.Fields, 0, f => ((Field)f).IsSystem = true);
                    break;

                case "design":
                    designNode = child;
                    break;
            }
        }

        if (designNode != null)
        {
            foreach (XmlNode child in designNode.ChildNodes)
            {
                switch (child.Name.ToLower())
                {
                    case "fields":
                        DeserializeFields(child, (Fields)metadataDom.Design.Fields, 0,
                            field => { ((Fields)document.Fields).Add(field.Reference ?? field); });
                        break;

                    case "types":
                        DeserializeMetadataTypes(child, (MetadataTypes)metadataDom.Design.Types, 0);
                        break;

                    case "pages":
                        DeserializePages(child, (Pages)metadataDom.Design.Pages);
                        break;

                    case "properties":
                        DeserializeProperties(child, metadataDom.Design.Properties);
                        break;

                    case "routings":
                        DeserializeRoutings(child, (Routings)metadataDom.Design.Routings);
                        break;
                }
            }
        }

        foreach (var cb in CallbacksToCorrectElementListDepth)
        {
            cb.Invoke(document);
        }

        CallbacksToCorrectElementListDepth.Clear();
    }

    public IMetadataDocument Deserialize(string fileName)
    {
        var metadataDom = new MetadataDocument();
        Deserialize(fileName, metadataDom);
        return metadataDom;
    }

    private delegate T DeserializeNodeCallback<out T>(XmlNode node);

    private delegate void DeserializeToObjectCallback<in T>(XmlNode node, T obj);

    private static void DeserializeMetadataDocumentInfo(XmlNode node, MetadataDocument document)
    {
        if (node.Attributes == null)
        {
            return;
        }

        foreach (XmlAttribute attr in node.Attributes)
        {
            switch (attr.Name.ToLower())
            {
                case "mdm_createversion":
                    document.CreateVersion = attr.Value;
                    break;

                case "mdm_lastversion":
                    document.LastVersion = attr.Value;
                    break;

                case "id":
                    document.Id = attr.Value;
                    break;

                case "data_version":
                    if (int.TryParse(attr.Value, out var iVer))
                    {
                        document.DataVersion = iVer;
                    }

                    break;

                case "data_sub_version":
                    if (int.TryParse(attr.Value, out var iSubVer))
                    {
                        document.DataSubVersion = iSubVer;
                    }

                    break;

                case "systemvariable":
                    document.SystemVariable = GetBooleanValue(attr.Value);
                    break;

                case "dbfiltervalidation":
                    document.DbFilterValidation = GetBooleanValue(attr.Value);
                    break;
            }
        }
    }

    private readonly struct MetadataPropertyNodeInfo(
        string? name,
        string? value,
        string? type,
        string? context,
        string? ds)
        : IEquatable<MetadataPropertyNodeInfo>
    {
        public static readonly MetadataPropertyNodeInfo Empty = new(null, null, null, null, null);

        public readonly string? Name = name;
        public readonly string? Context = context;
        public readonly string? Value = value;
        public readonly string? Type = type;
        public readonly string? Ds = ds;

        [MemberNotNullWhen(true, nameof(Name), nameof(Context), nameof(Value), nameof(Type))]
        public bool IsSimpleProperty()
        {
            return !string.IsNullOrEmpty(Name) &&
                   !string.IsNullOrEmpty(Context) &&
                   Value != null &&
                   !string.IsNullOrEmpty(Type);
        }

        [MemberNotNullWhen(true, nameof(Name), nameof(Context), nameof(Type))]
        public bool IsPropertyHasChild()
        {
            return !string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Context)
                                               && !string.IsNullOrEmpty(Type) && Value == null;
        }

        public bool Equals(MetadataPropertyNodeInfo other)
        {
            return Name == other.Name &&
                   Value == other.Value &&
                   Type == other.Type &&
                   Context == other.Context &&
                   Ds == other.Ds;
        }

        public override bool Equals(object? obj)
        {
            return obj is MetadataPropertyNodeInfo info && Equals(info);
        }

        public override int GetHashCode()
        {
            return HashCode.Combine(Name, Context, Value, Type, Ds);
        }

        public static bool operator ==(MetadataPropertyNodeInfo left, MetadataPropertyNodeInfo right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(MetadataPropertyNodeInfo left, MetadataPropertyNodeInfo right)
        {
            return !left.Equals(right);
        }
    }

    private static MetadataPropertyNodeInfo GetPropertyNodeInfo(XmlNode node)
    {
        if (!node.Name.Equals("property", StringComparison.OrdinalIgnoreCase) || node.Attributes == null)
        {
            return MetadataPropertyNodeInfo.Empty;
        }

        string? name = null;
        string? context = null;
        string? value = null;
        string? type = null;
        string? ds = null;

        foreach (XmlAttribute attr in node.Attributes)
        {
            switch (attr.Name.ToLower())
            {
                case "name":
                    name = attr.Value;
                    break;

                case "context":
                    context = attr.Value;
                    break;

                case "value":
                    value = attr.Value;
                    break;

                case "type":
                    type = attr.Value;
                    break;

                case "ds":
                    ds = attr.Value;
                    break;
            }
        }

        return new(name, value, type, context, ds);
    }

    // 1.
    //    <DIFF OP="ADD" name="1" Index="16">
    //        <text context="" xml:lang=""><![CDATA[]]></text>
    //    </DIFF>
    // 2.
    //    <version name="1">
    //        <labels context="LABEL"/>
    //    </version>
    private static void DeserializeVersionObject<T>(XmlNode node, VersionObject versionObject,
        DeserializeNodeCallback<T>? callback)
    {
        if (node.Attributes == null)
        {
            return;
        }

        var nodeName = node.Name.ToLower();

        if (nodeName == "diff" && callback != null)
        {
            foreach (XmlAttribute attr in node.Attributes)
            {
                switch (attr.Name.ToLower())
                {
                    case "op":
                        var opValue = attr.Value.ToLower();
                        versionObject.Operation =
                            opValue == "add" ? VersionOperations.Add : VersionOperations.Delete;
                        break;

                    case "name":
                        versionObject.Name = attr.Value;
                        break;

                    case "index":
                        TrySetIntValue(attr.Value, i => versionObject.Index = i);
                        break;

                    case "item":
                        versionObject.ItemName = attr.Value;
                        break;
                }
            }

            if (node.FirstChild != null)
            {
                versionObject.Data = callback(node.FirstChild!)!;
            }
        }
        else if (nodeName == "version")
        {
            var name = node.Attributes["name"]?.Value;

            if (string.IsNullOrEmpty(name))
            {
                return;
            }

            versionObject.Name = name!;

            if (node.FirstChild?.Attributes == null)
            {
                return;
            }

            foreach (XmlAttribute attr in node.FirstChild.Attributes)
            {
                versionObject.SetPropertyValue(attr.Name, attr.Value);
            }
        }
    }


    private static void DeserializeVersionObjects<T>(XmlNode node, VersionObjectCollection versionObjects,
        DeserializeNodeCallback<T> callback)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            var childName = child.Name.ToLower();

            if (childName != "diff" && childName != "version")
            {
                continue;
            }

            var versionObj = (VersionObject)versionObjects.AddNew();
            DeserializeVersionObject(child, versionObj, callback);

            if (string.IsNullOrEmpty(versionObj.Name))
            {
                continue;
            }

            var version = versionObjects.Document.VersionList.GetByKey(versionObj.Name);
            version.Add(versionObj);
        }
    }


    private static void DeserializeVersionObjects(XmlNode node, VersionObjectCollection versionObjects)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            if (!child.Name.Equals("version", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            var versionObject = (VersionObject)versionObjects.AddNew();
            DeserializeVersionObject<object>(child, versionObject, null);

            if (string.IsNullOrEmpty(versionObject.Name))
            {
                continue;
            }

            var version = versionObjects.Document.VersionList.GetByKey(versionObject.Name);
            version.Add(versionObject);
        }
    }


    // <properties>
    //     <versions>
    //          <DIFF OP="ADD" name="1" Index="0">
    //              <property name="" value="" type="" context="" ds=""/>
    //          </DIFF>
    //     </versions>
    //     <property name="" value="" type="" context="" ds=""/>
    // </properties>
    private static void DeserializeProperties(XmlNode node, IProperties? properties)
    {
        if (properties == null)
        {
            return;
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "versions":
                    var propObj = (Properties)properties;
                    DeserializeVersionObjects(child,
                        (VersionObjectCollection)(propObj.Versions ??=
                            new VersionObjectCollection(properties, properties.Document)), DeserializeProperty);
                    break;

                case "property":
                    var prop = DeserializeProperty(child);
                    if (prop != null && !string.IsNullOrEmpty(prop.Name))
                    {
                        ((Properties)properties).Add(prop);
                    }

                    break;

                case "unversioned":
                    DeserializeProperties(child,
                        ((Properties)properties).Unversioned ??
                        (((Properties)properties).Unversioned = new Properties(properties.Document)));
                    break;
            }
        }

        return;

        // <property name="" value="" type="" context=""/>
        IProperty? DeserializeProperty(XmlNode n)
        {
            var info = GetPropertyNodeInfo(n);

            if (!info.IsSimpleProperty())
            {
                return null;
            }

            var property = (Property)properties.CreateObject(info.Name!, info.Context!);

            switch (info.Type)
            {
                case "3":
                    property.SetValue(int.Parse(info.Value!));
                    break;
                case "5":
                    property.SetValue(double.Parse(info.Value!));
                    break;
                case "8":
                    property.SetValue(info.Value!);
                    break;
                case "11":
                    property.SetValue(GetBooleanValue(info.Value!));
                    break;
            }

            if (!string.IsNullOrEmpty(info.Ds) &&
                properties.Document.DataSources.TryGetByKey(info.Ds!, out var dataSource))
            {
                property.DataSource = dataSource;
            }

            if (n.FirstChild != null && n.FirstChild.Name.Equals("properties", StringComparison.OrdinalIgnoreCase))
            {
                DeserializeProperties(n.FirstChild, (Properties)property.Properties);
            }

            return property;
        }
    }

    //
    // <notes>
    //     <property name="5@note1" value="'''''''''''" type="8" context="Question" />
    //     <property name="5@note2" value="''''''''''''''''''''''''''''''''''''''''''''''''''''''   " type="8"
    //         context="Question" />
    //     <property name="9@note1" value=" Not being used - Commented by..." type="8" context="Question" />
    // </notes>
    //

    private static void DeserializeNote(XmlNode node, Notes notes)
    {
        if (node.Attributes == null)
        {
            return;
        }

        var name = node.Attributes["name"]?.Value ?? "";
        var value = node.Attributes["value"]?.Value ?? "";
        var context = node.Attributes["context"]?.Value;

        if (string.IsNullOrEmpty(context))
        {
            return;
        }

        notes.Add(name, value, context!);
    }

    private static void DeserializeNotes(XmlNode node, Notes notes)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            if (child.Name.Equals("property", StringComparison.OrdinalIgnoreCase))
            {
                DeserializeNote(child, notes);
            }
        }
    }

    // <templates>
    //  	<property name = "Question" value="HtmlTemplates/Question.htm" type="8" context="Question" />
    //  	<property name = "Banner" value="HtmlTemplates/Banner.htm" type="8" context="Question" />
    //  	<property name = "NavBar" value="HtmlTemplates/Nav.htm" type="8" context="Question" />
    //  	<property name = "Layout" value="HtmlTemplates/LAF.htm" type="8" context="Question" />
    // </templates>
    private static void DeserializeTemplates(XmlNode node, Templates templates)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            var info = GetPropertyNodeInfo(child);

            if (!info.IsSimpleProperty())
            {
                continue;
            }

            var temp = (Template)templates.Get(info.Name!, info.Context!);
            temp.Value = info.Value ?? string.Empty;
        }
    }

    //
    // Xml Example
    // <property name="Cell" type="9" context="WebApp">
    //     <styles>
    //         <property name="BorderColor" value="blue" type="8" context="WebApp" />
    //         <property name="BorderStyle" value="2" type="3" context="WebApp" />
    //         <property name="CWidth" value="2" type="8" context="WebApp" />
    //         <property name="CHeight" value="1. 234 PX" type="8" context="WebApp" />
    //         <property name="BorderTopColor" value="yellow" type="8" context="WebApp" />
    //         <property name="BorderRightColor" value="white" type="8" context="WebApp" />
    //         <property name="BorderBottomColor" value="blue" type="8" context="WebApp" />
    //         <property name="BorderLeftColor" value="black" type="8" context="WebApp" />
    //         <property name="BorderWidth" value="11" type="3" context="WebApp" />
    //         <property name="BorderTopWidth" value="10" type="3" context="WebApp" />
    //         <property name="BorderRightWidth" value="12" type="3" context="WebApp" />
    //         <property name="BorderBottomWidth" value="1" type="3" context="WebApp" />
    //         <property name="BorderLeftWidth" value="23" type="3" context="WebApp" />
    //         <property name="BorderTopStyle" value="5" type="3" context="WebApp" />
    //         <property name="BorderRightStyle" value="4" type="3" context="WebApp" />
    //         <property name="BorderBottomStyle" value="2" type="3" context="WebApp" />
    //         <property name="BorderLeftStyle" value="1" type="3" context="WebApp" />
    //         <property name="Padding" value="2" type="3" context="WebApp" />
    //         <property name="PaddingTop" value="1" type="3" context="WebApp" />
    //         <property name="PaddingRight" value="2" type="3" context="WebApp" />
    //         <property name="PaddingBottom" value="1" type="3" context="WebApp" />
    //         <property name="PaddingLeft" value="2" type="3" context="WebApp" />
    //         <property name="Wrap" value="0" type="11" context="WebApp" />
    //         <property name="ColSpan" value="1" type="3" context="WebApp" />
    //         <property name="RowSpan" value="1" type="3" context="WebApp" />
    //         <property name="CBgColor" value="blue" type="8" context="WebApp" />
    //         <property name="RepeatHeader" value="0" type="3" context="WebApp" />
    //     </styles>
    // </property>
    //

    private static void DeserializeCellStyle(XmlNode node, CellStyle cellStyle)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            var info = GetPropertyNodeInfo(child);

            if (!info.IsSimpleProperty())
            {
                continue;
            }

            switch (info.Name!.ToLower())
            {
                case "bordercolor":
                    cellStyle.BorderColor = info.Value;
                    break;

                case "borderstyle":
                    TrySetEnumValue<BorderStyles>(info.Value, style => cellStyle.BorderStyle = style);
                    break;

                case "cwidth":
                    cellStyle.Width = info.Value;
                    break;

                case "cheight":
                    cellStyle.Height = info.Value;
                    break;

                case "bordertopcolor":
                    cellStyle.BorderTopColor = info.Value;
                    break;

                case "borderrightcolor":
                    cellStyle.BorderRightColor = info.Value;
                    break;

                case "borderbottomcolor":
                    cellStyle.BorderBottomColor = info.Value;
                    break;

                case "borderleftcolor":
                    cellStyle.BorderLeftColor = info.Value;
                    break;

                case "borderwidth":
                    TrySetDoubleValue(info.Value, d => cellStyle.BorderWidth = d);
                    break;

                case "bordertopwidth":
                    TrySetDoubleValue(info.Value, d => cellStyle.BorderTopWidth = d);
                    break;

                case "borderrightwidth":
                    TrySetDoubleValue(info.Value, d => cellStyle.BorderRightWidth = d);
                    break;

                case "borderbottomwidth":
                    TrySetDoubleValue(info.Value, d => cellStyle.BorderBottomWidth = d);
                    break;

                case "borderleftwidth":
                    TrySetDoubleValue(info.Value, d => cellStyle.BorderLeftWidth = d);
                    break;

                case "bordertopstyle":
                    TrySetEnumValue<BorderStyles>(info.Value, style => cellStyle.BorderTopStyle = style);
                    break;

                case "borderrightstyle":
                    TrySetEnumValue<BorderStyles>(info.Value, style => cellStyle.BorderRightStyle = style);
                    break;

                case "borderbottomstyle":
                    TrySetEnumValue<BorderStyles>(info.Value, style => cellStyle.BorderBottomStyle = style);
                    break;

                case "borderleftstyle":
                    TrySetEnumValue<BorderStyles>(info.Value, style => cellStyle.BorderLeftStyle = style);
                    break;

                case "padding":
                    TrySetDoubleValue(info.Value, d => cellStyle.Padding = d);
                    break;

                case "paddingtop":
                    TrySetDoubleValue(info.Value, d => cellStyle.PaddingTop = d);
                    break;

                case "paddingright":
                    TrySetDoubleValue(info.Value, d => cellStyle.PaddingRight = d);
                    break;

                case "paddingbottom":
                    TrySetDoubleValue(info.Value, d => cellStyle.PaddingBottom = d);
                    break;

                case "paddingleft":
                    TrySetDoubleValue(info.Value, d => cellStyle.PaddingLeft = d);
                    break;

                case "wrap":
                    cellStyle.Wrap = GetBooleanValue(info.Value);
                    break;

                case "colspan":
                    TrySetIntValue(info.Value, i => cellStyle.ColSpan = i);
                    break;

                case "rowspan":
                    TrySetIntValue(info.Value, i => cellStyle.RowSpan = i);
                    break;

                case "cbgcolor":
                    cellStyle.BgColor = info.Value;
                    break;

                case "repeatheader":
                    TrySetIntValue(info.Value, i => cellStyle.RepeatHeader = i);
                    break;

                case "repeatsideheader":
                    TrySetIntValue(info.Value, i => cellStyle.RepeatSideHeader = i);
                    break;
            }
        }
    }

    //
    // <property name="Font" type="9" context="WebApp">
    //     <styles>
    //         <property name="Family" value="arial" type="8" context="WebApp" />
    //         <property name="Effects" value="45" type="3" context="WebApp" />
    //         <property name="Size" value="3" type="3" context="WebApp" />
    //     </styles>
    // </property>
    //

    private static void DeserializeFontStyle(XmlNode node, FontStyle fontStyle)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            var info = GetPropertyNodeInfo(child);

            if (!info.IsSimpleProperty())
            {
                continue;
            }

            switch (info.Name!.ToLower())
            {
                case "family":
                    fontStyle.Family = info.Value;
                    break;

                case "size":
                    TrySetDoubleValue(info.Value, d => fontStyle.Size = d);
                    break;

                case "effects":
                    if (int.TryParse(info.Value, out var i))
                    {
                        fontStyle.IsUnderline = (i & (int)FontStyleEffects.IsUnderline) != 0;
                        fontStyle.IsItalic = (i & (int)FontStyleEffects.IsItalic) != 0;
                        fontStyle.IsBold = (i & (int)FontStyleEffects.IsBold) != 0;
                        fontStyle.IsStrikethrough = (i & (int)FontStyleEffects.IsStrikethrough) != 0;
                        fontStyle.IsOverline = (i & (int)FontStyleEffects.IsOverline) != 0;
                        fontStyle.IsBlink = (i & (int)FontStyleEffects.IsBlink) != 0;
                        fontStyle.IsSubscript = (i & (int)FontStyleEffects.IsSubscript) != 0;
                        fontStyle.IsSuperscript = (i & (int)FontStyleEffects.IsSuperscript) != 0;
                    }

                    break;
            }
        }
    }

    //
    // Control Style Example
    // <property name="Control" type="9" context="WebApp">
    //     <styles>
    //         <property name="Accelerator" value="R" type="8" context="WebApp" />
    //         <property name="Type" value="14" type="3" context="WebApp" />
    //         <property name="ReadOnly" value="0" type="11" context="WebApp" />
    //     </styles>
    // </property>
    //

    private static void DeserializeControlStyle(XmlNode node, ControlStyle controlStyle)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            var info = GetPropertyNodeInfo(child);

            if (!info.IsSimpleProperty())
            {
                continue;
            }

            switch (info.Name!.ToLower())
            {
                case "accelerator":
                    controlStyle.Accelerator = info.Value;
                    break;

                case "readonly":
                    controlStyle.ReadOnly = GetBooleanValue(info.Value);
                    break;

                case "type":
                    TrySetEnumValue<ControlTypes>(info.Value, t => controlStyle.Type = t);
                    break;
            }
        }
    }

    //
    // Full Style Example
    // <styles>
    //     <property name="Cell" type="9" context="WebApp">
    //         <styles>
    //             <property name="BorderColor" value="blue" type="8" context="WebApp" />
    //             <property name="BorderStyle" value="2" type="3" context="WebApp" />
    //             <property name="CWidth" value="2" type="8" context="WebApp" />
    //             <property name="CHeight" value="1. 234 PX" type="8" context="WebApp" />
    //             <property name="BorderTopColor" value="yellow" type="8" context="WebApp" />
    //             <property name="BorderRightColor" value="white" type="8" context="WebApp" />
    //             <property name="BorderBottomColor" value="blue" type="8" context="WebApp" />
    //             <property name="BorderLeftColor" value="black" type="8" context="WebApp" />
    //             <property name="BorderWidth" value="11" type="3" context="WebApp" />
    //             <property name="BorderTopWidth" value="10" type="3" context="WebApp" />
    //             <property name="BorderRightWidth" value="12" type="3" context="WebApp" />
    //             <property name="BorderBottomWidth" value="1" type="3" context="WebApp" />
    //             <property name="BorderLeftWidth" value="23" type="3" context="WebApp" />
    //             <property name="BorderTopStyle" value="5" type="3" context="WebApp" />
    //             <property name="BorderRightStyle" value="4" type="3" context="WebApp" />
    //             <property name="BorderBottomStyle" value="2" type="3" context="WebApp" />
    //             <property name="BorderLeftStyle" value="1" type="3" context="WebApp" />
    //             <property name="Padding" value="2" type="3" context="WebApp" />
    //             <property name="PaddingTop" value="1" type="3" context="WebApp" />
    //             <property name="PaddingRight" value="2" type="3" context="WebApp" />
    //             <property name="PaddingBottom" value="1" type="3" context="WebApp" />
    //             <property name="PaddingLeft" value="2" type="3" context="WebApp" />
    //             <property name="Wrap" value="0" type="11" context="WebApp" />
    //             <property name="ColSpan" value="1" type="3" context="WebApp" />
    //             <property name="RowSpan" value="1" type="3" context="WebApp" />
    //             <property name="CBgColor" value="blue" type="8" context="WebApp" />
    //             <property name="RepeatHeader" value="0" type="3" context="WebApp" />
    //         </styles>
    //     </property>
    //     <property name="Control" type="9" context="WebApp">
    //         <styles>
    //             <property name="Accelerator" value="R" type="8" context="WebApp" />
    //             <property name="Type" value="14" type="3" context="WebApp" />
    //             <property name="ReadOnly" value="0" type="11" context="WebApp" />
    //         </styles>
    //     </property>
    //     <property name="Font" type="9" context="WebApp">
    //         <styles>
    //             <property name="Family" value="arial" type="8" context="WebApp" />
    //             <property name="Effects" value="45" type="3" context="WebApp" />
    //             <property name="Size" value="3" type="3" context="WebApp" />
    //         </styles>
    //     </property>
    //     <property name="Color" value="Blue" type="8" context="WebApp" />
    //     <property name="BgColor" value="blue" type="8" context="WebApp" />
    //     <property name="Hidden" value="0" type="11" context="WebApp" />
    //     <property name="Align" value="2" type="3" context="WebApp" />
    //     <property name="VerticalAlign" value="4" type="3" context="WebApp" />
    //     <property name="ElementAlign" value="2" type="3" context="WebApp" />
    //     <property name="Indent" value="3" type="3" context="WebApp" />
    //     <property name="ZIndex" value="3" type="3" context="WebApp" />
    //     <property name="Cursor" value="12" type="3" context="WebApp" />
    //     <property name="ImagePosition" value="0" type="3" context="WebApp" />
    //     <property name="Orientation" value="1" type="3" context="WebApp" />
    //     <property name="Columns" value="2" type="3" context="WebApp" />
    //     <property name="Rows" value="3" type="3" context="WebApp" />
    //     <property name="Width" value="4in" type="8" context="WebApp" />
    //     <property name="Height" value="23%" type="8" context="WebApp" />
    //     <property name="Cell" type="9" context="Question">
    //         <styles>
    //             <property name="BorderColor" value="blue" type="8" context="Question" />
    //             <property name="BorderStyle" value="2" type="3" context="Question" />
    //         </styles>
    //     </property>
    //     <property name="Color" value="Blue" type="8" context="Question" />
    //     <property name="Cell" type="9" context="SAV">
    //         <styles>
    //             <property name="BorderColor" value="blue" type="8" context="SAV" />
    //             <property name="BorderStyle" value="2" type="3" context="SAV" />
    //         </styles>
    //     </property>
    //     <property name="Color" value="Blue" type="8" context="SAV" />
    // </styles>
    //

    private static void UpdateStyleProperty(XmlNode node, Styles styles, ref Style? current)
    {
        var info = GetPropertyNodeInfo(node);

        if (string.IsNullOrEmpty(info.Context))
        {
            return;
        }

        if (current == null || !current.Context.Equals(info.Context, StringComparison.OrdinalIgnoreCase))
        {
            current = styles.GetOrAdd(info.Context!);
        }

        var style = current;

        if (info.IsSimpleProperty())
        {
            switch (info.Name!.ToLower())
            {
                case "color":
                    style.Color = info.Value;
                    break;

                case "bgcolor":
                    style.BgColor = info.Value;
                    break;

                case "hidden":
                    style.IsHidden = GetBooleanValue(info.Value);
                    break;

                case "align":
                    TrySetEnumValue<HorizontalAlignments>(info.Value, a => style.Align = a);
                    break;

                case "verticalalign":
                    TrySetEnumValue<VerticalAlignments>(info.Value, v => style.VerticalAlign = v);
                    break;

                case "elementalign":
                    TrySetEnumValue<ElementAlignments>(info.Value, e => style.ElementAlign = e);
                    break;

                case "indent":
                    TrySetIntValue(info.Value, i => style.Indent = i);
                    break;

                case "zindex":
                    TrySetIntValue(info.Value, i => style.ZIndex = i);
                    break;

                case "cursor":
                    TrySetEnumValue<CursorTypes>(info.Value, c => style.Cursor = c);
                    break;

                case "image":
                    style.Image = info.Value;
                    break;

                case "imageposition":
                    TrySetEnumValue<ImagePositions>(info.Value, i => style.ImagePosition = i);
                    break;

                case "orientation":
                    TrySetEnumValue<Orientations>(info.Value, o => style.Orientation = o);
                    break;

                case "columns":
                    TrySetIntValue(info.Value, i => style.Columns = i);
                    break;

                case "rows":
                    TrySetIntValue(info.Value, i => style.Rows = i);
                    break;

                case "width":
                    style.Width = info.Value;
                    break;

                case "height":
                    style.Height = info.Value;
                    break;
            }
        }
        else if (info.IsPropertyHasChild())
        {
            switch (info.Name!.ToLower())
            {
                case "cell":
                    if (node.FirstChild != null)
                    {
                        DeserializeCellStyle(node.FirstChild, style.Cell ??= new CellStyle());
                    }

                    break;

                case "control":
                    if (node.FirstChild != null)
                    {
                        DeserializeControlStyle(node.FirstChild,
style.Control ??= new ControlStyle());
                    }

                    break;

                case "font":
                    if (node.FirstChild != null)
                    {
                        DeserializeFontStyle(node.FirstChild, style.Font ??= new FontStyle());
                    }

                    break;
            }
        }
    }

    private static void DeserializeStyles(XmlNode node, Styles styles)
    {
        Style? current = null;
        foreach (XmlNode child in node.ChildNodes)
        {
            UpdateStyleProperty(child, styles, ref current);
        }
    }

    //
    // <labels context="LABEL">
    //     <text context="WEBAPP" xml:lang="zh-CN">category</text>
    //     <text context="QUESTION" xml:lang="zh-CN">category</text>
    //     <text context="SAV" xml:lang="zh-CN">category</text>
    // </labels>
    //

    private static void DeserializeLabels(XmlNode node, MetadataLabels labels)
    {
        XmlAttribute? lblCtx;

        if (node.Attributes == null || (lblCtx = node.Attributes["context"]) == null)
        {
            return;
        }

        var label = (MetadataLabel)labels.GetOrAdd(lblCtx.Value);

        foreach (XmlNode child in node.ChildNodes)
        {
            if (!child.Name.Equals("text", StringComparison.OrdinalIgnoreCase) ||
                child.Attributes == null)
            {
                continue;
            }

            var ctx = child.Attributes["context"]?.Value;
            var lang = child.Attributes["xml:lang"]?.Value;

            if (string.IsNullOrEmpty(ctx) || string.IsNullOrEmpty(lang))
            {
                continue;
            }

            string text;
            bool cdata;

            if (child.FirstChild?.NodeType == XmlNodeType.CDATA)
            {
                text = child.FirstChild.InnerText;
                cdata = true;
            }
            else
            {
                text = child.InnerText;
                cdata = false;
            }

            label.SetText(ctx!, lang!, text, cdata);
        }
    }

    private static bool DeserializeLabeledObject(XmlNode node, IMetadataLabeledObject obj)
    {
        switch (node.Name.ToLower())
        {
            case "notes":
                DeserializeNotes(node, (Notes)obj.Notes);
                return true;

            case "properties":
                DeserializeProperties(node, (Properties)obj.Properties);
                return true;

            case "templates":
                DeserializeTemplates(node, (Templates)obj.Templates);
                return true;

            case "styles":
                DeserializeStyles(node, (Styles)(obj.Style ??= new Styles(obj.Document, obj)));
                return true;

            case "labelstyles":
                DeserializeStyles(node,
                    (Styles)(obj.LabelStyle ??= new Styles(obj.Document, obj)));
                return true;

            case "labels":
                DeserializeLabels(node, (MetadataLabels)obj.Label);
                return true;
        }

        return false;
    }

    private static void DeserializeCategoryMap(XmlNode node, CategoryMap categoryMap)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            if (!child.Name.Equals("categoryid", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            var name = child.Attributes?["name"]?.Value;
            var value = child.Attributes?["value"]?.Value;

            if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(value) ||
                !int.TryParse(value, out var iVal))
            {
                continue;
            }

            categoryMap.TryAdd(name!, iVal);
        }
    }

    private static void DeserializeVersionList(XmlNode node, VersionList versionList)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            if (child.Attributes == null)
            {
                continue;
            }

            var name = child.Attributes["name"]?.Value;

            if (string.IsNullOrEmpty(name))
            {
                continue;
            }

            var version = (Version)versionList.GetByKey(name!);

            foreach (XmlAttribute attr in child.Attributes)
            {
                switch (attr.Name.ToLower())
                {
                    case "label":
                        version.Label = attr.Value;
                        break;

                    case "description":
                        version.Description = attr.Value;
                        break;

                    case "createdby":
                        version.CreateBy = attr.Value;
                        break;

                    case "id":
                        version.Id = attr.Value;
                        break;

                    case "datecreated":
                        if (DateTime.TryParse(attr.Value, out var dt))
                        {
                            version.DateCreated = dt;
                        }

                        break;

                    case "mdmversion":
                        version.MdmVersion = attr.Value;
                        break;

                    case "locked":
                        if (GetBooleanValue(attr.Value))
                        {
                            version.Lock();
                        }

                        break;

                    case "datelocked":
                        if (DateTime.TryParse(attr.Value, out var lockDt))
                        {
                            version.DateLocked = lockDt;
                        }

                        break;

                    case "major":
                        version.Major = attr.Value;
                        break;

                    case "minor":
                        version.Minor = attr.Value;
                        break;
                }
            }
        }
    }

    private static void DeserializeAtoms(XmlNode node, IList<string> atomList)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            var name = child.Attributes?["name"]?.Value;

            if (!child.Name.Equals("atom", StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(name))
            {
                continue;
            }

            atomList.Add(name!);
        }
    }

    private static void DeserializeContextLike<T>(XmlNode node, T obj, Action<XmlNode>? callback) where T : IContext
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "properties":
                    DeserializeProperties(child, obj.Properties);
                    break;

                case "alternatives":
                    foreach (XmlNode alt in child.ChildNodes)
                    {
                        var name = alt.Attributes?["name"]?.Value;
                        if (!string.IsNullOrEmpty(name))
                        {
                            obj.SetAlternative(name!);
                        }
                    }

                    break;
            }

            callback?.Invoke(child);
        }
    }

    private static void DeserializeContexts<T, TInstance>(XmlNode node, ContextsBase<T> contexts,
        DeserializeToObjectCallback<TInstance> callback, string childTag)
        where T : class, IContext
        where TInstance : T
    {
        var baseName = node.Attributes?["base"]?.Value;

        if (string.IsNullOrEmpty(baseName))
        {
            return;
        }

        XmlNode? versionsNode = null;
        contexts.BaseName = baseName!;

        foreach (XmlNode child in node.ChildNodes)
        {
            if (child.Name.Equals("versions", StringComparison.OrdinalIgnoreCase))
            {
                versionsNode = child;
                continue;
            }

            var name = child.Attributes?["name"]?.Value;

            if (string.IsNullOrEmpty(name))
            {
                continue;
            }

            var ctx = (TInstance)contexts.GetByKey(name!);
            callback(child, ctx);
        }

        if (versionsNode != null)
        {
            DeserializeVersionObjects(versionsNode,
                (VersionObjectCollection)(contexts.Versions ??=
                    new VersionObjectCollection(contexts, contexts.Document)), DeserializeContextItem);
        }

        return;

        TInstance? DeserializeContextItem(XmlNode child)
        {
            if (!childTag.Equals(child.Name, StringComparison.OrdinalIgnoreCase))
            {
                return default;
            }

            var name = child.Attributes?["name"]?.Value;

            if (string.IsNullOrEmpty(name))
            {
                return default;
            }

            var item = (TInstance)contexts.CreateObject(name!);
            callback(child, item);
            return item;
        }
    }

    private static void DeserializeContext<T>(XmlNode node, T context)
        where T : Context
    {
        DeserializeContextLike(node, context, null);
    }

    private static void DeserializeLanguage(XmlNode node, Language language)
    {
        DeserializeContextLike(node, language, null);

        var id = node.Attributes?["id"]?.Value;

        if (!string.IsNullOrEmpty(id))
        {
            language.Id = id!;
        }
    }

    // <user name="DMSRun.exe" fileversion="2.1.1.109" comment="Executable of the process MDM run under" />

    private static void DeserializeMdmUser(XmlNode node, SaveLog saveLog)
    {
        if (node.Attributes == null)
        {
            return;
        }

        string? name = null;
        string? fileVersion = null;
        var comment = string.Empty;

        foreach (XmlAttribute attr in node.Attributes)
        {
            switch (attr.Name.ToLower())
            {
                case "name":
                    name = attr.Value;
                    break;

                case "fileversion":
                    fileVersion = attr.Value;
                    break;

                case "comment":
                    comment = attr.Value;
                    break;
            }
        }

        if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(fileVersion))
        {
            saveLog.Users.AddNew(name!, fileVersion!, comment);
        }
    }

    //
    // <savelog fileversion="6.0.1.3.107" versionset="" username="svcABextraction" date="3/15/2021 2:21:12 PM" count="4">
    //     <user name="DMSRun.exe" fileversion="2.1.1.109" comment="Executable of the process MDM run under" />
    // </savelog>
    //

    private static void DeserializeSaveLog(XmlNode node, SaveLogs saveLogs)
    {
        if (node.Attributes == null)
        {
            return;
        }

        string? fileVersion = null;
        var versionSet = string.Empty;
        var userName = string.Empty;
        string? date = null;
        var count = 0;

        foreach (XmlAttribute attr in node.Attributes)
        {
            switch (attr.Name.ToLower())
            {
                case "fileversion":
                    fileVersion = attr.Value;
                    break;

                case "versionset":
                    versionSet = attr.Value;
                    break;

                case "username":
                    userName = attr.Value;
                    break;

                case "date":
                    date = attr.Value;
                    break;

                case "count":
                    if (int.TryParse(attr.Value, out var iCount))
                    {
                        count = iCount;
                    }

                    break;
            }
        }

        if (string.IsNullOrEmpty(fileVersion))
        {
            return;
        }

        var saveLog = (SaveLog)saveLogs.AddNew(fileVersion!, count);
        saveLog.VersionSetName = versionSet;
        saveLog.UserName = userName;

        if (!string.IsNullOrEmpty(date) && DateTime.TryParse(date, out var dt))
        {
            saveLog.Date = dt;
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            DeserializeMdmUser(child, saveLog);
        }
    }

    //
    // <scripttype type="mrScriptBasic" context="PAPER" interviewmodes="0" usekeycodes="0" />
    // <scripttype type="mrScriptBasic" context="PAPER">
    //     <deleted />
    //     <versions>
    //         <version name="1">
    //             <scripttype type="mrScriptBasic" context="PAPER" />
    //         </version>
    //     </versions>
    //     <script name="main" default="-1">""</script>
    // </scripttype>
    //

    private static MetadataScript? DeserializeScript(XmlNode node)
    {
        if (node.Attributes == null)
        {
            return null;
        }

        var name = node.Attributes["name"]?.Value;
        var isDefault = node.Attributes["default"]?.Value;

        if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(isDefault))
        {
            return null;
        }

        var item = new MetadataScript(name!)
        {
            IsDefault = GetBooleanValue(isDefault),
            Script = node.InnerText
        };
        return item;
    }


    private static void DeserializeScriptType(XmlNode node, MetadataScriptType scriptType)
    {
        if (node.Attributes == null)
        {
            return;
        }

        var interviewModes = node.Attributes["interviewmodes"]?.Value;
        var useKeycodes = node.Attributes["usekeycodes"]?.Value;

        if (!string.IsNullOrEmpty(interviewModes))
        {
            scriptType.InterviewModes = Enum.Parse<InterviewModes>(interviewModes, true);
        }

        if (!string.IsNullOrEmpty(useKeycodes))
        {
            scriptType.UseKeyCode = GetBooleanValue(useKeycodes);
        }

        XmlNode? versionNode = null;

        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "deleted":
                    foreach (XmlNode child2 in child.ChildNodes)
                    {
                        if (!child2.Name.Equals("script", StringComparison.OrdinalIgnoreCase))
                        {
                            continue;
                        }

                        var item = DeserializeScript(child2);

                        if (item != null)
                        {
                            scriptType.AddDeletedScript(item);
                        }
                    }

                    break;

                case "script":
                    var script = DeserializeScript(child);

                    if (script != null)
                    {
                        scriptType.Add(script);
                    }

                    break;

                case "versions":
                    versionNode = child;
                    break;
            }
        }

        if (versionNode != null)
        {
            DeserializeVersionObjects(versionNode,
                (VersionObjectCollection)(scriptType.Versions ??= new VersionObjectCollection(scriptType,
                                                                    scriptType.Document)),
                DeserializeScript);
        }
    }

    //
    // <routings name="plans">
    //     <scripts>
    //         <deleted>
    //             <scripttype type="mrScriptBasic" context="PAPER">
    //                 <deleted />
    //                 <versions>
    //                     <version name="1">
    //                         <scripttype type="mrScriptBasic" context="PAPER" />
    //                     </version>
    //                 </versions>
    //             </scripttype>
    //         </deleted>
    //         <versions>
    //             <DIFF OP="ADD" name="1" Index="0">
    //                 <scripttype type="mrScriptBasic" context="PAPER" />
    //             </DIFF>
    //             <DIFF OP="DEL" name="1" Index="0" />
    //         </versions>
    //     </scripts>
    //     <versions>
    //         <version name="1">
    //             <routings name="plans" />
    //         </version>
    //     </versions>
    //     <routing context="PAPER" interviewmodes="0" usekeycodes="0">
    //         <versions>
    //             <DIFF OP="ADD" name="1" Index="0">
    //                 <ritem name="plan_view@galleries_multi" item="7b5fd45e-a4fe-4b61-bd52-a904e8584c12" />
    //             </DIFF>
    //             <DIFF OP="ADD" name="1" Index="1">
    //                 <ritem name="pl_order" item="f2ddcdb6-e5ab-4e5c-b94b-ab154614b566" />
    //             </DIFF>
    //             <DIFF OP="ADD" name="1" Index="2">
    //                 <ritem name="rating_ent" item="30b18459-a5fd-4fb9-ad60-1b338e086e0c" />
    //             </DIFF>
    //         </versions>
    //         <ritem name="plan_view@galleries_multi" item="7b5fd45e-a4fe-4b61-bd52-a904e8584c12" />
    //         <ritem name="pl_order" item="f2ddcdb6-e5ab-4e5c-b94b-ab154614b566" />
    //         <ritem name="rating_ent" item="30b18459-a5fd-4fb9-ad60-1b338e086e0c" />
    //     </routing>
    // </routings>
    //

    private static MetadataScriptType? DeserializeScriptTypeNode(XmlNode node, IMetadataDocument document)
    {
        if (node.Attributes == null)
        {
            return null;
        }

        var type = node.Attributes["type"]?.Value;
        var context = node.Attributes["context"]?.Value;
        var interviewModes = node.Attributes["interviewmodes"]?.Value;
        var useKeycodes = node.Attributes["usekeycodes"]?.Value;

        if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(context))
        {
            return null;
        }

        var scriptType = new MetadataScriptType(document, type!, context!);

        if (!string.IsNullOrEmpty(interviewModes))
        {
            scriptType.InterviewModes = Enum.Parse<InterviewModes>(interviewModes, true);
        }

        if (!string.IsNullOrEmpty(useKeycodes))
        {
            scriptType.UseKeyCode = GetBooleanValue(useKeycodes);
        }

        DeserializeScriptType(node, scriptType);

        return scriptType;
    }

    private static void DeserializeScripts(XmlNode node, MetadataScripts scripts)
    {
        XmlNode? versionNode = null;

        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "scripttype":
                    var st = DeserializeScriptTypeNode(child, scripts.Document);
                    if (st != null)
                    {
                        scripts.Add(st);
                    }

                    break;

                case "deleted":
                    foreach (XmlNode child2 in child.ChildNodes)
                    {
                        var delst = DeserializeScriptTypeNode(child2, scripts.Document);
                        if (delst != null)
                        {
                            scripts.AddDeletedScriptType(delst);
                        }
                    }

                    break;

                case "versions":
                    versionNode = child;
                    break;
            }
        }

        if (versionNode != null)
        {
            DeserializeVersionObjects(
                versionNode,
                (VersionObjectCollection)(scripts.Versions ??=
                    new VersionObjectCollection(scripts, scripts.Document)),
                n => DeserializeScriptTypeNode(n, scripts.Document));
        }
    }

    //
    // <ritem name="plan@YesNo" item="b47ade7b-95fa-44f8-bab4-f83e43ac7538">
    //     <gotolist>
    //         <goto name="NA" ref="52" />
    //         <goto name="NO" ref="49" />
    //         <goto name="YES" ref="52" />
    //     </gotolist>
    // </ritem>
    //

    private static void DeserializeGotoList(XmlNode node, IGotoList gotoList)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            var name = child.Attributes?["name"]?.Value;
            var reference = child.Attributes?["ref"]?.Value;

            if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(reference) ||
                !int.TryParse(reference, out var index))
            {
                continue;
            }

            gotoList.AddNew(name!, index);
        }
    }

    private static void DeserializeRouting(XmlNode node, Routings routings)
    {
        if (node.Attributes == null)
        {
            return;
        }

        var context = node.Attributes["context"]?.Value;
        var interViewModes = node.Attributes["interviewmodes"]?.Value;
        var useKeyCodes = node.Attributes["usekeycodes"]?.Value;

        if (string.IsNullOrEmpty(context) || string.IsNullOrEmpty(interViewModes) ||
            string.IsNullOrEmpty(useKeyCodes))
        {
            return;
        }

        var routing = (Routing)routings.AddNew();

        routing.Context = context!;
        routing.InterviewModes = Enum.Parse<InterviewModes>(interViewModes);
        routing.UseKeyCodes = GetBooleanValue(useKeyCodes);

        XmlNode? versionsNode = null;

        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "versions":
                    versionsNode = child;
                    break;

                case "ritem":
                    DeserializeRoutingItem(child);
                    break;
            }
        }

        if (versionsNode != null)
        {
            DeserializeVersionObjects(versionsNode,
                (VersionObjectCollection)(routing.Versions ??=
                    new VersionObjectCollection(routing, routing.Document)), DeserializeRoutingItem);
        }

        return;

        RoutingItem? DeserializeRoutingItem(XmlNode n)
        {
            if (!n.Name.Equals("ritem", StringComparison.OrdinalIgnoreCase))
            {
                return null;
            }

            var name = n.Attributes?["name"]?.Value;
            var item = n.Attributes?["item"]?.Value;

            if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(item))
            {
                return null;
            }

            var routingItem = (RoutingItem)routing.Items.CreateObject(name!);
            routingItem.ItemVariableId = item!;

            foreach (XmlNode child in n.ChildNodes)
            {
                switch (child.Name.ToLower())
                {
                    case "gotolist":
                        DeserializeGotoList(child, routingItem.Goto);
                        break;
                }
            }

            return routingItem;
        }
    }

    private static void DeserializeRoutings(XmlNode node, Routings routings)
    {
        var name = node.Attributes?["name"]?.Value;
        if (!string.IsNullOrEmpty(name))
        {
            routings.Name = name!;
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "properties":
                    DeserializeProperties(child, routings.Properties);
                    break;

                case "scripts":
                    DeserializeScripts(child, (MetadataScripts)routings.Scripts);
                    break;

                case "routing":
                    DeserializeRouting(child, routings);
                    break;

                case "versions":
                    DeserializeVersionObjects(child,
                        (VersionObjectCollection)(routings.Versions ??=
                            new VersionObjectCollection(routings, routings.Document)));
                    break;
            }
        }
    }

    private static void DeserializeAlias(XmlNode node, AliasMap aliasMap)
    {
        if (node.Attributes == null)
        {
            return;
        }

        var fullName = node.Attributes["fullname"]?.Value;
        var aliasName = node.Attributes["aliasname"]?.Value ?? "";

        if (string.IsNullOrEmpty(fullName))
        {
            return;
        }

        var alias = (Alias)aliasMap.AddNew(fullName!, aliasName);

        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "nativevalues":
                    foreach (XmlNode nativeValueNode in child.ChildNodes)
                    {
                        if (nativeValueNode.Attributes == null)
                        {
                            continue;
                        }

                        var nativeFullName = nativeValueNode.Attributes["fullname"]?.Value;
                        var nativeValue = nativeValueNode.Attributes["value"]?.Value;

                        if (string.IsNullOrEmpty(nativeFullName) || string.IsNullOrEmpty(nativeValue))
                        {
                            continue;
                        }

                        alias.AddNativeValue(fullName!, nativeValue!);
                    }

                    break;

                case "subalias":
                    var index = child.Attributes?["index"]?.Value;
                    var name = child.Attributes?["name"]?.Value;
                    if (!string.IsNullOrEmpty(index) && !string.IsNullOrEmpty(name) &&
                        int.TryParse(index, out var intIndex))
                    {
                        alias.AddSubAlias(intIndex, name!);
                    }

                    break;

                case "properties":
                    DeserializeProperties(child, alias.Properties);
                    break;
            }
        }
    }

    private static void DeserializeDataSource(XmlNode node, DataSources dataSources)
    {
        if (node.Attributes == null)
        {
            return;
        }

        var name = node.Attributes["name"]?.Value;
        var dbLocation = node.Attributes["dblocation"]?.Value;
        var cDscName = node.Attributes["cdscname"]?.Value;
        var project = node.Attributes["project"]?.Value ?? "";
        var id = node.Attributes["id"]?.Value;

        if (string.IsNullOrEmpty(name))
        {
            return;
        }

        var ds = (DataSource)dataSources.CreateObject(name!);
        ds.Project = project;

        if (!string.IsNullOrEmpty(dbLocation))
        {
            ds.DbLocation = dbLocation!;
        }

        if (!string.IsNullOrEmpty(cDscName))
        {
            ds.Cdsc = cDscName!;
        }

        if (!string.IsNullOrEmpty(id))
        {
            ds.Id = id!;
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "document_properties":
                    DeserializeProperties(child, ds.Properties);
                    break;

                case "var":
                    DeserializeAlias(child, (AliasMap)ds.AliasMap);
                    break;
            }
        }

        dataSources.Add(ds);
    }

    private static void DeserializeDataSources(XmlNode node, DataSources dataSources)
    {
        var defaultName = node.Attributes?["default"]?.Value;

        if (string.IsNullOrEmpty(defaultName))
        {
            return;
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            var lowerChildName = child.Name.ToLower();
            if (lowerChildName == "connection")
            {
                DeserializeDataSource(child, dataSources);
            }
        }

        dataSources.DefaultName = defaultName!;
    }

    private static object GetXmlPropertyValue(string value, string? valueType)
    {
        return valueType switch
        {
            "11" => GetBooleanValue(value),
            "5" => double.TryParse(value, out var dValue) ? dValue : 0,
            "3" => int.TryParse(value, out var iValue) ? iValue : 0,
            _ => value
        };
    }


    //
    // <multiplier-variable id="_cd29e664-72d8-401c-ad2c-0d900bcff3c2" name="excact"
    //     ref="cd29e664-72d8-401c-ad2c-0d900bcff3c2" />
    // <othervariable id="_cd29e664-72d8-401c-ad2c-0d900bcff3c2" name="excact" ref="cd29e664-72d8-401c-ad2c-0d900bcff3c2" />
    //

    private static Variable? DeserializeVariableReference(XmlNode node, IMetadataDocument document, int levelDepth)
    {
        if (node.Attributes == null)
        {
            return null;
        }

        var name = node.Attributes["name"]?.Value ?? string.Empty;
        var id = node.Attributes["id"]?.Value;
        var refId = node.Attributes["ref"]?.Value;

        if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(refId))
        {
            return null;
        }

        return new Variable(document, name, refId!)
        {
            Id = id!,
            LevelDepth = levelDepth + 1
        };
    }

    //
    // <mappings>
    //     <varinstance name="Qname" sourcetype="8" variable="c16359db-d21d-4133-afde-28b8d0641ece"
    //         fullname="Camloop[{LPs_HR_C1}].Qname" />
    //     <varinstance name="bk_Wave" variable="2379" fullname="bk_Wave" />
    //     <varinstance name="questi2" variable="f26c281f-1a48-4c66-8d6a-99d6c5570a9f" fullname="FREQUENZ[{BLACK}].Questi2">
    //         <categories>
    //             <element fullname="less" />
    //             <element fullname="na" />
    //             <element fullname="oneprday" />
    //             <element fullname="oneprmonth" />
    //             <element fullname="oneprweek" />
    //             <element fullname="twotothreeprweek" />
    //             <deleted />
    //         </categories>
    //         <source />
    //     </varinstance>
    // </mappings>
    //

    private static ElementInstance? DeserializeElementInstance(XmlNode node, IMetadataDocument document)
    {
        var fullName = node.Attributes?["fullname"]?.Value;

        if (string.IsNullOrEmpty(fullName))
        {
            return null;
        }

        var instance = new ElementInstance(document, fullName!);

        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "properties":
                    DeserializeProperties(child, instance.Properties);
                    break;
            }
        }

        return instance;
    }

    private static void DeserializeElementInstances(XmlNode node, ElementInstances instances)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "deleted":
                    break;

                case "element":
                    var ins = DeserializeElementInstance(child, instances.Document);
                    if (ins != null)
                    {
                        instances.Add(ins);
                    }

                    break;

                case "versions":
                    DeserializeVersionObjects(child,
                        (VersionObjectCollection)(instances.Versions ??=
                            new VersionObjectCollection(instances, instances.Document)),
                        n => DeserializeElementInstance(n, instances.Document));
                    break;
            }
        }
    }

    private static void DeserializeVariableInstanceInMap(XmlNode node, VariableInstances variables)
    {
        if (node.Attributes == null)
        {
            return;
        }

        var name = node.Attributes["name"]?.Value;
        var variable = node.Attributes["variable"]?.Value;
        var fullName = node.Attributes["fullname"]?.Value;

        if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(variable))
        {
            return;
        }

        var instance = (VariableInstance)variables.CreateObject(name!, variable!, fullName);

        var sourceType = node.Attributes["sourcetype"]?.Value;
        if (!string.IsNullOrEmpty(sourceType))
        {
            instance.SourceType = Enum.Parse<SourceTypes>(sourceType);
        }

        var unversioned = node.Attributes["unversioned"]?.Value;
        if (!string.IsNullOrEmpty(unversioned))
        {
            instance.Versioned = !GetBooleanValue(unversioned);
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "properties":
                    DeserializeProperties(child, instance.Properties);
                    break;

                case "categories":
                    DeserializeElementInstances(child, (ElementInstances)instance.Elements);
                    break;

                case "versions":
                    DeserializeVersionObjects(child,
                        (VersionObjectCollection)(instance.Versions ??=
                            new VersionObjectCollection(instance, instance.Document)));
                    break;
            }
        }

        variables.Add(instance);
    }

    private static void DeserializeVariableInstanceMap(XmlNode node, VariableInstances variableInstances)
    {
        foreach (XmlNode child in node.ChildNodes)
        {
            DeserializeVariableInstanceInMap(child, variableInstances);
        }
    }

    private static Element? DeserializeElement(XmlNode node, Elements elements)
    {
        if (node.Attributes == null)
        {
            return null;
        }

        var id = node.Attributes["id"]?.Value;
        var name = node.Attributes["name"]?.Value;

        if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(name))
        {
            return null;
        }

        var element = (Element)elements.CreateObject(name!);

        if (!string.IsNullOrEmpty(id))
        {
            element.Id = id!;
        }

        foreach (XmlAttribute attr in node.Attributes)
        {
            switch (attr.Name.ToLower())
            {
                case "type":
                    element.ElementType = Enum.Parse<ElementType>(attr.Value);
                    break;

                case "expression":
                    element.Expression = attr.Value;
                    break;

                case "fixed":
                    if (GetBooleanValue(attr.Value))
                    {
                        element.Flag |= CategoryFlags.FixedPosition;
                    }

                    break;

                case "factor-value":
                    var facType = node.Attributes["factor-type"]?.Value;
                    if (!string.IsNullOrEmpty(facType))
                    {
                        element.Factor = GetXmlPropertyValue(attr.Value, facType);
                    }
                    else
                    {
                        element.Factor = int.TryParse(attr.Value, out var iValue) ? iValue :
                            double.TryParse(attr.Value, out var dValue) ? dValue : attr.Value;
                    }

                    break;

                case "keycode":
                    element.KeyCode = attr.Value;
                    break;

                case "nofilter":
                    if (GetBooleanValue(attr.Value))
                    {
                        element.Flag |= CategoryFlags.NoFilter;
                    }

                    break;

                case "exclusive":
                    if (GetBooleanValue(attr.Value))
                    {
                        element.Flag |= CategoryFlags.Exclusive;
                    }

                    break;

                case "inline":
                    if (GetBooleanValue(attr.Value))
                    {
                        element.Flag |= CategoryFlags.Inline;
                    }

                    break;

                case "multiplier-local":
                    if (GetBooleanValue(attr.Value))
                    {
                        element.Flag |= CategoryFlags.Multiplier;
                    }

                    break;

                case "multiplier":
                    element.Flag |= CategoryFlags.Multiplier;
                    element.MultiplierVariableId = attr.Value;
                    break;

                case "other-local":
                    if (GetBooleanValue(attr.Value))
                    {
                        element.Flag |= CategoryFlags.Other;
                    }

                    break;

                case "other":
                    element.Flag |= CategoryFlags.Other;
                    element.OtherVariableId = attr.Value;
                    break;

                case "missing":
                    switch (attr.Value.ToLower())
                    {
                        case "noanswer":
                            element.Flag |= CategoryFlags.NoAnswer;
                            break;

                        case "dontknow":
                            element.Flag |= CategoryFlags.DontKnow;
                            break;

                        case "refuse":
                            element.Flag |= CategoryFlags.Refuse;
                            break;
                    }

                    break;

                case "derived":
                    element.IsDerived = GetBooleanValue(attr.Value);
                    break;

                case "categoriesref":
                    if (!string.IsNullOrEmpty(attr.Value))
                    {
                        element.CategoriesReference = attr.Value;
                    }

                    break;

                case "ref_name":
                    element.ReferenceName = attr.Value;
                    break;

                case "global-name-space":
                    element.Namespace = !GetBooleanValue(attr.Value);
                    break;
            }
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            if (DeserializeLabeledObject(child, element))
            {
                continue;
            }

            switch (child.Name.ToLower())
            {
                case "multiplier-variable":
                    var multiReference = DeserializeVariableReference(child, element.Document,
                        elements.Owner?.LevelDepth ?? 0);
                    if (multiReference != null)
                    {
                        ((Variables)element.LocalMultiplierVariables).Add(multiReference);
                    }

                    break;

                case "othervariable":
                    var otherReference = DeserializeVariableReference(child, element.Document,
                        elements.Owner?.LevelDepth ?? 0);
                    if (otherReference != null)
                    {
                        ((Variables)element.LocalOtherVariables).Add(otherReference);
                    }

                    break;

                case "categories":
                    element.ElementType = ElementType.CategoryList;
                    goto case "category";

                case "element":
                case "category":
                    var childElement = DeserializeElement(child, (Elements)element.Elements);
                    if (childElement != null)
                    {
                        ((Elements)element.Elements).Add(childElement);

                        if (childElement.ElementType == ElementType.Category && elements.Owner != null)
                        {
                            ((Categories)elements.Owner.Categories).Add(childElement);
                        }
                    }

                    break;
            }
        }

        return element;
    }

    private static ElementList? DeserializeElementListNode(XmlNode node, MetadataTypes types)
    {
        if (node.Attributes == null)
        {
            return null;
        }

        var id = node.Attributes["id"]?.Value;
        var name = node.Attributes["name"]?.Value;

        if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(name))
        {
            return null;
        }

        var list = (ElementList)types.CreateObject(name!);

        if (!string.IsNullOrEmpty(id))
        {
            list.Id = id!;
        }

        var globalNamespace = node.Attributes["global-name-space"]?.Value;
        if (!string.IsNullOrEmpty(globalNamespace))
        {
            list.Namespace = !GetBooleanValue(globalNamespace);
        }

        var refId = node.Attributes["ref"]?.Value;
        if (!string.IsNullOrEmpty(refId))
        {
            list.ReferenceId = refId!;

            CallbacksToCorrectElementListDepth.Add(d =>
            {
                if (!d.Types.TryGetByKey(refId!, out var defList))
                {
                    return;
                }

                var parentField = d.FindParent<IField>();
                if (parentField != null)
                {
                    ((ElementList)defList).LevelDepth = parentField.LevelDepth + 1;
                }
            });

            return list;
        }

        // categoriesref
        var categoriesRef = node.Attributes["categoriesref"]?.Value;
        var referenceName = node.Attributes["ref_name"]?.Value;

        if (!string.IsNullOrEmpty(categoriesRef) && !string.IsNullOrEmpty(referenceName))
        {
            list.Elements.ReferenceId = categoriesRef;
            list.Elements.ReferenceName = referenceName;
            return list;
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            if (DeserializeLabeledObject(node, list))
            {
                continue;
            }

            if (!child.Name.Equals("element", StringComparison.OrdinalIgnoreCase) &&
                !child.Name.Equals("category", StringComparison.OrdinalIgnoreCase) &&
                !child.Name.Equals("categories", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            var element = DeserializeElement(child, (Elements)list.Elements);
            if (element != null)
            {
                ((Elements)list.Elements).Add(element);
            }
        }

        return list;
    }

    private static void DeserializeElementList(XmlNode node, MetadataTypes elementLists, int levelDepth,
        Action<ElementList>? callback = null)
    {
        var list = DeserializeElementListNode(node, elementLists);
        if (list == null)
        {
            return;
        }

        list.LevelDepth = levelDepth + 1;
        elementLists.Add(list);
        callback?.Invoke(list);
    }

    private static void DeserializeElements(XmlNode node, Elements elements)
    {
        if (node.Attributes != null)
        {
            foreach (XmlAttribute attr in node.Attributes)
            {
                switch (attr.Name.ToLower())
                {
                    case "global-name-space":
                        elements.Namespace = !GetBooleanValue(attr.Value);
                        break;

                    case "fixed":
                        elements.Fixed = GetBooleanValue(attr.Value);
                        break;

                    case "derived":
                        elements.IsDerived = GetBooleanValue(attr.Value);
                        break;

                    case "categoriesref":
                        elements.ReferenceId = attr.Value;
                        break;

                    case "ref_name":
                        elements.ReferenceName = attr.Value;
                        break;
                }
            }
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            if (child.Name.Equals("properties", StringComparison.OrdinalIgnoreCase))
            {
                DeserializeProperties(child, elements.Properties);
                continue;
            }

            var element = DeserializeElement(child, elements);

            if (element == null)
            {
                continue;
            }

            elements.Add(element);

            if (element.ElementType == ElementType.Category && elements.Owner != null)
            {
                ((Categories)elements.Owner.Categories).Add(element);
            }
        }
    }

    private static void DeserializeHelperFields(XmlNode node, HelperFields? helperFields)
    {
        if (helperFields == null)
        {
            return;
        }

        var globalNs = node.Attributes?["global-name-space"]?.Value;
        if (!string.IsNullOrEmpty(globalNs))
        {
            helperFields.Namespace = !GetBooleanValue(globalNs);
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            switch (child.Name.ToLower())
            {
                case "variable":
                    var instance = DeserializeVariableReference(child, helperFields.Document,
                        ((IField)helperFields.Parent).LevelDepth);
                    if (instance != null)
                    {
                        helperFields.Add(instance);
                    }

                    break;

                case "deleted":
                    foreach (XmlNode child2 in child.ChildNodes)
                    {
                        var delInstance = DeserializeVariableReference(child2, helperFields.Document,
                            ((IField)helperFields.Parent).LevelDepth);
                        if (delInstance == null)
                        {
                            continue;
                        }

                        delInstance.Parent = helperFields;
                        helperFields.AddDeletedItem(delInstance);
                    }

                    break;

                case "versions":
                    DeserializeVersionObjects(
                        child,
                        (VersionObjectCollection)(helperFields.Versions ??=
                            new VersionObjectCollection(helperFields, helperFields.Document)),
                        xmlNode => DeserializeVariableReference(xmlNode, helperFields.Document,
                            ((IField)helperFields.Parent).LevelDepth));
                    break;
            }
        }
    }

    private static Orientations GetOrientations(IProperties properties)
    {
        return (properties.GetPropertyValue<string>("Question", "DisplayOrientation")?.ToLower()) switch
        {
            "vertical" => Orientations.Column,
            "horizontal" => Orientations.Row,
            _ => Orientations.Default,
        };
    }

    // 最上级变量定义的LevelDepth一直是0.
    private static void DeserializeVariable(XmlNode node, Variables variables)
    {
        if (node.Attributes == null)
        {
            return;
        }

        var name = node.Attributes["name"]?.Value;
        var id = node.Attributes["id"]?.Value;

        if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(id))
        {
            return;
        }

        var usageType = node.Attributes["usagetype"]?.Value;

        // <multiplier-variable> 和 <othervariable> 允许同时出现
        if (Enum.TryParse(usageType, out VariableUsage usage) &&
            (usage & (VariableUsage.Multiplier | VariableUsage.OtherSpecify)) != 0 &&
            !string.IsNullOrEmpty(id) &&
            variables.TryGetByKey(id!.ToLower(), out var exist))
        {
            ((Variable)exist).UsageType = usage;
            return;
        }

        var variable = (Variable)variables.CreateObject(name!);
        variable.UsageType = usage;

        if (!string.IsNullOrEmpty(id))
        {
            variable.Id = id!;
        }

        object? minValue = null;
        object? maxValue = null;

        foreach (XmlNode attr in node.Attributes)
        {
            if (string.IsNullOrEmpty(attr.Value))
            {
                continue;
            }

            switch (attr.Name.ToLower())
            {
                case "infotype":
                    variable.FieldInfo = attr.Value.ParseEnum<FieldInfoTypes>(true);
                    break;

                case "type":
                    variable.DataType = attr.Value.ParseEnum<FieldDataType>();
                    break;

                case "min":
                    var minType = node.Attributes["mintype"]?.Value;
                    minValue = GetXmlPropertyValue(attr.Value, minType);
                    variable.MinValue = minValue;
                    break;

                case "max":
                    var maxType = node.Attributes["maxtype"]?.Value;
                    maxValue = GetXmlPropertyValue(attr.Value, maxType);
                    variable.MaxValue = maxValue;
                    break;

                case "initialanswer":
                    var initType = node.Attributes["initialanswer_type"]?.Value;
                    variable.InitialAnswer = GetXmlPropertyValue(attr.Value, initType);
                    break;

                case "defaultanswer":
                    var defaultType = node.Attributes["defaultanswer_type"]?.Value;
                    variable.DefaultAnswer = GetXmlPropertyValue(attr.Value, defaultType);
                    break;

                case "rangeexp":
                    variable.Ranges.AddExpression(attr.Value);
                    break;

                case "precisions":
                    if (int.TryParse(attr.Value, out var precision))
                    {
                        variable.Precision = precision;
                    }

                    break;

                case "expression":
                    variable.Expression = attr.Value;
                    break;

                case "scale":
                    if (int.TryParse(attr.Value, out var scale))
                    {
                        variable.Scale = scale;
                    }

                    break;

                case "validation":
                    variable.Validation = attr.Value;
                    break;

                case "no-casedata":
                    variable.NoCaseData = GetBooleanValue(attr.Value);
                    break;

                case "isdb":
                    variable.IsDbQuestion = GetBooleanValue(attr.Value);
                    break;

                case "dbtype":
                    variable.DbQuestionType = attr.Value.ParseEnum<DbQuestionTypes>(true);
                    break;

                case "dbdatatype":
                    variable.DbDataType = attr.Value.ParseEnum<FieldDataType>(true);
                    break;

                case "unversioned":
                    variable.Versioned = !GetBooleanValue(attr.Value);
                    break;

                case "autoelements":
                    variable.AutoElements = GetBooleanValue(attr.Value);
                    break;
            }
        }

        if (minValue != null || maxValue != null)
        {
            variable.Ranges.Add(maxValue, minValue);
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            if (DeserializeLabeledObject(child, variable))
            {
                continue;
            }

            switch (child.Name.ToLower())
            {
                case "axis":
                    variable.AxisExpression = child.Attributes?["expression"]?.Value;
                    break;

                case "categories":
                    DeserializeElements(child, (Elements)variable.Elements);
                    break;

                case "db":
                    DeserializeProperties(child, variable.DataSourceProperties);
                    break;

                case "helperfields":
                    DeserializeHelperFields(child, (HelperFields?)variable.HelperFields);
                    break;

                case "versions":
                    DeserializeVersionObjects(
                        child,
                        (VersionObjectCollection)(variable.Versions ??=
                            new VersionObjectCollection(variable, variables.Document)));
                    break;
            }
        }

        variable.Orientation = GetOrientations(variable.Properties);
        variables.Add(variable);
    }

    private static PageItem? DeserializePageItem(XmlNode node, Page page)
    {
        var itemId = node.Attributes?["id"]?.Value;
        var itemName = node.Attributes?["name"]?.Value;
        var itemRef = node.Attributes?["ref"]?.Value;

        if (string.IsNullOrEmpty(itemName) || string.IsNullOrEmpty(itemRef))
        {
            return null;
        }

        var pageItem = (PageItem)page.CreateObject(itemName!);
        if (!string.IsNullOrEmpty(itemId))
        {
            pageItem.Id = itemId!;
        }

        pageItem.ReferenceId = itemRef!;

        return pageItem;
    }

    private static void DeserializePage(XmlNode node, Pages pages)
    {
        var page = DeserializePageNode(node, pages);
        if (page != null)
        {
            pages.Add(page);
        }
    }

    private static Page? DeserializePageNode(XmlNode node, Pages pages)
    {
        if (node.Attributes == null)
        {
            return null;
        }

        var id = node.Attributes["id"]?.Value;
        var name = node.Attributes["name"]?.Value;
        var globalNs = node.Attributes["global-name-space"]?.Value;

        if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(name))
        {
            return null;
        }

        var page = (Page)pages.CreateObject(name!);
        page.Id = id!;

        if (globalNs != null)
        {
            page.Namespace = !GetBooleanValue(globalNs);
        }

        var refId = node.Attributes["ref"]?.Value;
        if (!string.IsNullOrEmpty(refId))
        {
            page.ReferenceId = refId!;
            return page;
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            if (DeserializeLabeledObject(child, page))
            {
                continue;
            }

            switch (child.Name.ToLower())
            {
                case "item":
                    var pageItem = DeserializePageItem(child, page);
                    if (pageItem != null)
                    {
                        page.Add(pageItem);
                    }

                    break;

                case "deleted":
                    foreach (XmlNode child2 in child.ChildNodes)
                    {
                        var delItem = DeserializePageItem(child2, page);
                        if (delItem != null)
                        {
                            page.AddDeletedItem(delItem);
                        }
                    }

                    break;
            }
        }

        return page;
    }

    private static void DeserializePages(XmlNode node, Pages pages)
    {
        var globalNs = node.Attributes?["global-name-space"]?.Value;

        if (!string.IsNullOrEmpty(globalNs))
        {
            pages.Namespace = !GetBooleanValue(globalNs);
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            if (child.Name.Equals("deleted", StringComparison.OrdinalIgnoreCase))
            {
                foreach (XmlNode delNode in child.ChildNodes)
                {
                    var delPage = DeserializePageNode(delNode, pages);
                    if (delPage != null)
                    {
                        pages.AddDeletedItem(delPage);
                    }
                }
            }
            else
            {
                var page = DeserializePageNode(child, pages);
                if (page != null)
                {
                    pages.Add(page);
                }
            }
        }
    }

    private static void DeserializeMetadataTypes(XmlNode node, MetadataTypes types, int levelDepth)
    {
        var globalNs = node.Attributes?["global-name-space"]?.Value;

        if (!string.IsNullOrEmpty(globalNs))
        {
            types.Namespace = !GetBooleanValue(globalNs);
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            if (child.Name.Equals("deleted", StringComparison.OrdinalIgnoreCase))
            {
                foreach (XmlNode delNode in child.ChildNodes)
                {
                    var delList = DeserializeElementListNode(delNode, types);
                    if (delList != null)
                    {
                        types.Add(delList);
                    }
                }
            }
            else
            {
                DeserializeElementList(child, types, levelDepth);
            }
        }
    }

    private static void DeserializeMetadataClass(XmlNode node, MetadataClass metadataClass)
    {
        var globalNs = node.Attributes?["global-name-space"]?.Value;

        if (!string.IsNullOrEmpty(globalNs))
        {
            metadataClass.Namespace = !GetBooleanValue(globalNs);
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            if (DeserializeLabeledObject(child, metadataClass))
            {
                continue;
            }

            switch (child.Name.ToLower())
            {
                case "types":
                    DeserializeMetadataTypes(child, (MetadataTypes)metadataClass.Types, metadataClass.LevelDepth);
                    break;

                case "fields":
                    DeserializeFields(child, (Fields)metadataClass.Fields, metadataClass.LevelDepth);
                    break;

                case "pages":
                    DeserializePages(child, (Pages)metadataClass.Pages);
                    break;
            }
        }
    }

    private static MetadataArray? DeserializeMetadataArray(XmlNode node, Fields fields, int levelDepth)
    {
        if (node.Attributes == null)
        {
            return null;
        }

        var id = node.Attributes["id"]?.Value;
        var name = node.Attributes["name"]?.Value;

        if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(name))
        {
            return null;
        }

        var arr = (MetadataArray)fields.CreateArray(name!, id!);
        arr.LevelDepth = levelDepth + 1;
        ((MetadataClass)arr.Class).LevelDepth = arr.LevelDepth;

        foreach (XmlAttribute attr in node.Attributes)
        {
            switch (attr.Name.ToLower())
            {
                case "isgrid":
                    arr.IsGrid = GetBooleanValue(attr.Value);
                    break;

                case "isdb":
                    arr.IsDbQuestion = GetBooleanValue(attr.Value);
                    break;

                case "dbdatatype":
                    arr.DbDataType = attr.Value.ParseEnum<FieldDataType>(true);
                    break;

                case "dbtype":
                    arr.DbQuestionType = attr.Value.ParseEnum<DbQuestionTypes>(true);
                    break;

                case "type":
                    arr.ArrayType = attr.Value.ParseEnum<ArrayTypes>(true);
                    break;

                case "iteratortype":
                    arr.IteratorType = attr.Value.ParseEnum<IteratorTypes>(true);
                    break;

                case "rangeexp":
                    arr.Ranges.AddExpression(attr.Value);
                    break;

                case "autoelements":
                    arr.AutoElements = GetBooleanValue(attr.Value);
                    break;
            }
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            if (DeserializeLabeledObject(child, arr))
            {
                continue;
            }

            switch (child.Name.ToLower())
            {
                case "categories":
                    DeserializeElements(child, (Elements)arr.Elements);
                    break;

                case "class":
                    DeserializeMetadataClass(child, (MetadataClass)arr.Class);
                    break;

                case "helperfields":
                    DeserializeHelperFields(child, (HelperFields)arr.HelperFields);
                    break;

                case "ranges":
                    foreach (XmlNode rngNode in child.ChildNodes)
                    {
                        if (!rngNode.Name.Equals("range", StringComparison.OrdinalIgnoreCase))
                        {
                            continue;
                        }

                        var upper = rngNode.Attributes?["upperbound"]?.Value;
                        var lower = rngNode.Attributes?["lowerbound"]?.Value;

                        object? upperValue = upper;
                        object? lowerValue = lower;

                        if (string.IsNullOrEmpty(upper) || string.IsNullOrEmpty(lower))
                        {
                            continue;
                        }

                        if (int.TryParse(upper, out var upperVal))
                        {
                            if (upperVal == UpperInfinity)
                            {
                                upperValue = null;
                            }
                            else
                            {
                                upperValue = upperVal;
                            }
                        }

                        if (int.TryParse(lower, out var lowerVal))
                        {
                            if (lowerVal == LowerInfinity)
                            {
                                lowerValue = null;
                            }
                            else
                            {
                                lowerValue = lowerVal;
                            }
                        }

                        arr.Ranges.Add(upperValue, lowerValue);
                    }

                    break;

                case "versions":
                    DeserializeVersionObjects(child,
                        (VersionObjectCollection)(arr.Versions ??= new VersionObjectCollection(arr, arr.Document)));
                    break;
            }
        }

        arr.Orientation = GetOrientations(arr.Properties);
        return arr;
    }

    private static Compound? DeserializeCompound(XmlNode node, Fields fields, int levelDepth)
    {
        if (node.Attributes == null)
        {
            return null;
        }

        var name = node.Attributes["name"]?.Value;
        var id = node.Attributes["id"]?.Value;

        if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(id))
        {
            return null;
        }

        var compound = (Compound)fields.CreateCompound(name!, id!);
        compound.LevelDepth = levelDepth + 1;

        var globalNs = node.Attributes["global-name-space"]?.Value;

        if (!string.IsNullOrEmpty(globalNs))
        {
            compound.Namespace = !GetBooleanValue(globalNs);
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            if (DeserializeLabeledObject(child, compound))
            {
                continue;
            }

            switch (child.Name.ToLower())
            {
                case "categories":
                    DeserializeElements(child, (Elements)compound.Elements);
                    break;

                case "types":
                    DeserializeMetadataTypes(child, (MetadataTypes)compound.Types, compound.LevelDepth);
                    break;

                case "fields":
                    DeserializeFields(child, (Fields)compound.Fields, compound.LevelDepth);
                    break;

                case "pages":
                    DeserializePages(child, (Pages)compound.Pages);
                    break;

                case "routings":
                    DeserializeRoutings(child, (Routings)compound.Routings);
                    break;
            }
        }

        compound.Orientation = GetOrientations(compound.Properties);
        return compound;
    }

    private static Block? DeserializeBlock(XmlNode node, Fields fields, int levelDepth)
    {
        if (node.Attributes == null)
        {
            return null;
        }

        var name = node.Attributes["name"]?.Value;
        var id = node.Attributes["id"]?.Value;

        if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(id))
        {
            return null;
        }

        var block = (Block)fields.CreateBlock(name!, id!);
        block.LevelDepth = levelDepth + 1;

        var globalNs = node.Attributes["global-name-space"]?.Value;
        if (!string.IsNullOrEmpty(globalNs))
        {
            block.Namespace = GetBooleanValue(globalNs);
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            if (DeserializeLabeledObject(child, block))
            {
                continue;
            }

            switch (child.Name.ToLower())
            {
                case "fields":
                    DeserializeFields(child, (Fields)block.Fields, block.LevelDepth);
                    break;

                case "routings":
                    DeserializeRoutings(child, (Routings)block.Routings);
                    break;

                case "pages":
                    DeserializePages(child, (Pages)block.Pages);
                    break;
            }
        }

        return block;
    }

    private static IField? DeserializeFieldsElement(XmlNode node, Fields fields, int levelDepth)
    {
        return node.Name.ToLower() switch
        {
            "variable" => DeserializeVariableReference(node, fields.Document, levelDepth),
            "grid" or "loop" => DeserializeMetadataArray(node, fields, levelDepth),
            "compound" => DeserializeCompound(node, fields, levelDepth),
            "class" => DeserializeBlock(node, fields, levelDepth),
            _ => null,
        };
    }

    private static void DeserializeFields(XmlNode node, Fields fields, int levelDepth,
        Action<IField>? callback = null)
    {
        var globalNs = node.Attributes?["global-name-space"]?.Value;

        if (!string.IsNullOrEmpty(globalNs))
        {
            fields.Namespace = !GetBooleanValue(globalNs);
        }

        foreach (XmlNode child in node.ChildNodes)
        {
            var versioned = true;

            switch (child.Name.ToLower())
            {
                case "unversioned":
                    versioned = false;
                    goto case "deleted";

                case "deleted":
                    foreach (XmlNode child2 in child.ChildNodes)
                    {
                        var childField = DeserializeFieldsElement(child2, fields, levelDepth);
                        if (childField == null)
                        {
                            continue;
                        }

                        childField.Versioned = versioned;

                        if (!versioned)
                        {
                            fields.Add(childField);
                            callback?.Invoke(childField);
                        }
                        else
                        {
                            fields.AddDeletedItem(childField);
                        }
                    }

                    break;

                default:
                    var field = DeserializeFieldsElement(child, fields, levelDepth);
                    if (field != null)
                    {
                        fields.Add(field);
                        callback?.Invoke(field);
                    }

                    break;
            }
        }
    }

    #endregion

    #region Script Serialze Methods

    private int _indentDepth;
    private string _domCtx = string.Empty;
    private string _domLang = string.Empty;
    private string _domLbType = string.Empty;


    public string Serialize(IMetadataDocument document)
    {
        var builder = new StringBuilder();
        _domCtx = string.IsNullOrEmpty(Options.Context) ? document.Context : Options.Context!;
        _domLang = string.IsNullOrEmpty(Options.Language) ? document.Language : Options.Language!;
        _domLbType = string.IsNullOrEmpty(Options.LabelType) ? document.LabelType : Options.LabelType!;

        if (!Options.IgnoreMetadataSection)
        {
            builder.AppendLine($"Metadata({_domLang}, {_domCtx}, {_domLbType})");
            NextIndentDepth();
        }

        if (Options.HasMetadataElementFlag(MetadataElementFlags.HData) &&
            document.Properties.CountIf(_domCtx) > 0 &&
            document.Templates.CountIf(_domCtx) > 0)
        {
            AppendLineIndent(builder);
            builder.Append("HDATA");
            SerializeLabel(builder, document);
            builder.Append(';');
            builder.AppendLine();
        }

        foreach (var list in document.Design.Types)
        {
            SerializeElementList(builder, list);
            builder.AppendLine();
        }

        foreach (var field in document.Design.Fields)
        {
            SerializeField(builder, field);
            builder.AppendLine();
        }

        foreach (var page in document.Design.Pages)
        {
            SerializePage(builder, page);
            builder.AppendLine();
        }

        if (!Options.IgnoreMetadataSection)
        {
            PreviousIndentDepth();
            AppendLineIndent(builder);
            builder.Append("End Metadata");
        }

        return builder.ToString();
    }


    private void NextIndentDepth()
    {
        _indentDepth++;
    }

    private void PreviousIndentDepth()
    {
        if (_indentDepth > 0)
        {
            _indentDepth--;
        }
    }

    private void AppendLineIndent(StringBuilder builder)
    {
        builder.AppendLine();

        if (_indentDepth == 0 || Options.Indent == 0)
        {
            return;
        }

        builder.Append(' ', _indentDepth * Options.Indent);
    }

    private static void DropLastChar(StringBuilder builder, char c)
    {
        if (builder.Length > 0 && builder[^1] == c)
        {
            builder.Remove(builder.Length - 1, 1);
        }
    }

    private static void DropLastComma(StringBuilder builder)
    {
        if (builder.Length > 0 && builder[^1] == ',')
        {
            builder.Remove(builder.Length - 1, 1);
        }
    }

    private static void SerializeObjectValue(StringBuilder builder, object? value)
    {
        if (value == null)
        {
            return;
        }

        switch (value)
        {
            case string str:
                if (str.StartsWith('{') && str.EndsWith('}'))
                {
                    builder.Append(str);
                }
                else
                {
                    builder.Append($"\"{NormalizeString(str)}\"");
                }

                break;

            case bool bln:
                builder.Append(bln ? "true" : "false");
                break;

            default:
                builder.Append(value);
                break;
        }
    }

    private static void SerializePropertyValue(StringBuilder builder, IProperty property)
    {
        if (property.Value == null)
        {
            builder.Append("null");
            return;
        }

        switch (property.ValueType)
        {
            case PropertyValueType.Int:
                builder.Append((int)property.Value);
                break;
            case PropertyValueType.Double:
                builder.Append((double)property.Value);
                break;
            case PropertyValueType.Bool:
                builder.Append(property.Value is true);
                break;
            case PropertyValueType.String:
                builder.Append($@"""{(
                    property.Value is string str
                        ? NormalizeString(str) : property.Value)}""");
                break;
            case PropertyValueType.Properties:
                builder.Append("null");
                break;
            case PropertyValueType.Null:
            default:
                builder.Append(property.Value);
                break;
        }
    }

    private void SerializeProperty(StringBuilder builder, IProperty property)
    {
        AppendLineIndent(builder);
        builder.Append(property.Name);
        builder.Append(' ');
        builder.Append('=');
        builder.Append(' ');
        SerializePropertyValue(builder, property);
        builder.Append(',');
    }

    // [
    //     projectDescription = "",
    //     shellVersion = "3.3.5",
    //     buildServer = "KONAPMRI031",
    //     buildLib = "RazorLib",
    //     contacts = "",
    //     stakeholder = "MB",
    //     jobNumber = "7740",
    //     studyType = "AdHoc",
    //     Version = 2,
    //     versionComment = "Initial launch",
    //     themeName = "default",
    //     themeVersion = "2.2"
    // ]

    private bool SerializeProperties(StringBuilder builder, IProperties properties, bool hdata = false)
    {
        if (hdata || !Options.HasMetadataElementFlag(MetadataElementFlags.Properties))
        {
            return false;
        }

        var exportCount = 0;
        foreach (var prop in properties.Filter(_domCtx))
        {
            if (prop.Name.Equals("DisplayOrder", StringComparison.OrdinalIgnoreCase) ||
                prop.Name.Equals("DisplayOrientation", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            if (exportCount == 0)
            {
                NextIndentDepth();
                AppendLineIndent(builder);
                builder.Append('[');
                NextIndentDepth();
            }

            SerializeProperty(builder, prop);
            exportCount++;
        }

        if (exportCount <= 0)
        {
            return false;
        }

        DropLastComma(builder);
        PreviousIndentDepth();
        AppendLineIndent(builder);
        builder.Append(']');
        PreviousIndentDepth();

        return true;
    }

    // 写入类似 "PropertyName = Value," 格式的脚本内容
    private void SerializePropertyLike<T>(StringBuilder builder, string name, T value,
        Predicate<T>? predicate = null)
    {
        if (value == null || (predicate != null && !predicate(value)))
        {
            return;
        }

        AppendLineIndent(builder);
        builder.Append($"{name} = ");

        switch (value)
        {
            case string _:
            case Enum _:
                builder.Append('\"');
                builder.Append(value);
                builder.Append('\"');
                break;
            case bool bln:
                builder.Append(bln);
                break;
            default:
                builder.Append(value);
                break;
        }

        builder.Append(',');
    }

    //
    // templates(
    // Question = "HtmlTemplates/Question.htm",
    // Layout = "HtmlTemplates/LAF.htm",
    // navbar = "HtmlTemplates/Nav.htm",
    // banner = "HtmlTemplates/Banner.htm"
    // )
    //

    private bool SerializeTemplates(StringBuilder builder, ITemplates templates, bool hdata = false)
    {
        if (!Options.HasMetadataElementFlag(MetadataElementFlags.Templates) && !hdata)
        {
            return false;
        }

        var exportCount = 0;
        foreach (var template in templates.Filter(_domCtx))
        {
            if (exportCount == 0)
            {
                NextIndentDepth();
                AppendLineIndent(builder);
                builder.Append("templates (");
                NextIndentDepth();
            }

            SerializePropertyLike(builder, template.Name, template.Value);
            exportCount++;
        }

        if (exportCount <= 0)
        {
            return false;
        }

        DropLastComma(builder);
        PreviousIndentDepth();
        AppendLineIndent(builder);
        builder.Append(')');
        PreviousIndentDepth();

        return true;
    }

    private static bool IsControlStyleEmpty([NotNullWhen(false)] ControlStyle? control)
    {
        return control == null || (control.Type == null
                                   && control.Accelerator == null
                                   && control.ReadOnly == null);
    }

    private static bool IsCellStyleEmpty([NotNullWhen(false)] CellStyle? cellStyle)
    {
        return cellStyle == null || (string.IsNullOrEmpty(cellStyle.Width)
                                     && string.IsNullOrEmpty(cellStyle.Height)
                                     && string.IsNullOrEmpty(cellStyle.BgColor)
                                     && string.IsNullOrEmpty(cellStyle.BorderColor)
                                     && string.IsNullOrEmpty(cellStyle.BorderBottomColor)
                                     && string.IsNullOrEmpty(cellStyle.BorderLeftColor)
                                     && string.IsNullOrEmpty(cellStyle.BorderRightColor)
                                     && string.IsNullOrEmpty(cellStyle.BorderTopColor)
                                     && cellStyle.BorderStyle == null
                                     && cellStyle.BorderBottomStyle == null
                                     && cellStyle.BorderLeftStyle == null
                                     && cellStyle.BorderRightStyle == null
                                     && cellStyle.BorderTopStyle == null
                                     && cellStyle.BorderWidth == null
                                     && cellStyle.BorderBottomWidth == null
                                     && cellStyle.BorderLeftWidth == null
                                     && cellStyle.BorderRightWidth == null
                                     && cellStyle.BorderTopWidth == null
                                     && cellStyle.Padding == null
                                     && cellStyle.PaddingBottom == null
                                     && cellStyle.PaddingLeft == null
                                     && cellStyle.PaddingRight == null
                                     && cellStyle.PaddingTop == null
                                     && cellStyle.Wrap == null
                                     && cellStyle.ColSpan == null
                                     && cellStyle.RowSpan == null
                                     && cellStyle.RepeatHeader == null
                                     && cellStyle.RepeatSideHeader == null
            );
    }

    private static bool IsFontStyleEmpty([NotNullWhen(false)] FontStyle? style)
    {
        return style == null || (string.IsNullOrEmpty(style.Family)
                                 &&
                                 style is
                                 {
                                     Size: null,
                                     IsBlink: false,
                                     IsSuperscript: false,
                                     IsSubscript: false,
                                     IsStrikethrough: false,
                                     IsUnderline: false,
                                     IsOverline: false,
                                     IsItalic: false,
                                     IsBold: false
                                 });
    }

    private static bool IsStyleEmpty(Style style)
    {
        return string.IsNullOrEmpty(style.Color)
               && string.IsNullOrEmpty(style.BgColor)
               && style.IsHidden == null
               && style.Align == null
               && style.VerticalAlign == null
               && style.ElementAlign == null
               && style.Indent == null
               && style.ZIndex == null
               && style.Cursor == null
               && string.IsNullOrEmpty(style.Image)
               && style.ImagePosition == null
               && style.Orientation == null
               && style.Columns == null
               && style.Rows == null
               && style.Width == null
               && style.Height == null
               && IsControlStyleEmpty(style.Control)
               && IsCellStyleEmpty(style.Cell)
               && IsFontStyleEmpty(style.Font)
            ;
    }

    //
    // 完整脚本示例：
    //
    // style(
    // Color = "Blue",
    // BgColor = "blue",
    // Hidden = false,
    // Align = "Center",
    // VerticalAlign = "Super",
    // ElementAlign = "NewLine",
    // Indent = 3,
    // ZIndex = 3,
    // Cursor = "SEResize",
    // ImagePosition = "Left",
    // Orientation = "Row",
    // Columns = 2,
    // Rows = 3,
    // Width = "4in",
    // Height = "23%",
    // Control(
    // Type = "Password",
    // ReadOnly = false,
    // Accelerator = "R"
    // ),
    // Cell(
    // Width = "2",
    // Height = "1. 234 PX",
    // BorderColor = "blue",
    // BorderTopColor = "yellow",
    // BorderRightColor = "white",
    // BorderBottomColor = "blue",
    // BorderLeftColor = "black",
    // BorderStyle = "Double",
    // BorderWidth = 11,
    // BorderTopWidth = 10,
    // BorderRightWidth = 12,
    // BorderBottomWidth = 1,
    // BorderLeftWidth = 23,
    // BorderTopStyle = "Inset",
    // BorderRightStyle = "Ridge",
    // BorderBottomStyle = "Double",
    // BorderLeftStyle = "Solid",
    // Padding = 2,
    // PaddingTop = 1,
    // PaddingRight = 2,
    // PaddingBottom = 1,
    // PaddingLeft = 2,
    // Wrap = false,
    // ColSpan = 1,
    // RowSpan = 1,
    // BgColor = "blue",
    // RepeatHeader = 0
    // ),
    // Font(
    // Family = "arial",
    // Size = 3,
    // IsUnderline = true,
    // IsItalic = false,
    // IsBold = true,
    // IsStrikethrough = true,
    // IsOverline = false,
    // IsBlink = true,
    // IsSuperscript = false,
    // IsSubscript = false
    // )
    // ),
    // Styles ::= ( [ AreaName: ] style ( property (, property )* ) )*
    //

    private bool SerializeStyle(StringBuilder builder, IStyles? styles, string name)
    {
        if (styles == null || !Options.HasMetadataElementFlag(MetadataElementFlags.Styles))
        {
            return false;
        }

        var style = styles.Get(_domCtx);

        if (style == null || IsStyleEmpty(style))
        {
            return false;
        }

        NextIndentDepth();
        AppendLineIndent(builder);
        builder.Append(name);
        builder.Append(" (");
        NextIndentDepth();

        // Style properties

        SerializePropertyLike(builder, "Color", style.Color);
        SerializePropertyLike(builder, "BgColor", style.BgColor);
        SerializePropertyLike(builder, "Hidden", style.IsHidden);
        SerializePropertyLike(builder, "Align", style.Align);
        SerializePropertyLike(builder, "VerticalAlign", style.VerticalAlign);
        SerializePropertyLike(builder, "ElementAlign", style.ElementAlign);
        SerializePropertyLike(builder, "Indent", style.Indent);
        SerializePropertyLike(builder, "ZIndex", style.ZIndex);
        SerializePropertyLike(builder, "Cursor", style.Cursor);
        SerializePropertyLike(builder, "Image", style.Image);
        SerializePropertyLike(builder, "ImagePosition", style.ImagePosition);
        SerializePropertyLike(builder, "Orientation", style.Orientation);
        SerializePropertyLike(builder, "Columns", style.Columns);
        SerializePropertyLike(builder, "Rows", style.Rows);
        SerializePropertyLike(builder, "Width", style.Width);
        SerializePropertyLike(builder, "Height", style.Height);

        // Control Style

        if (!IsControlStyleEmpty(style.Control))
        {
            AppendLineIndent(builder);
            builder.Append("Control (");
            NextIndentDepth();
            SerializePropertyLike(builder, "Type", style.Control?.Type);
            SerializePropertyLike(builder, "ReadOnly", style.Control?.ReadOnly);
            SerializePropertyLike(builder, "Accelerator", style.Control?.Accelerator);
            PreviousIndentDepth();
            DropLastComma(builder);
            AppendLineIndent(builder);
            builder.Append("),");
        }

        // End Control Style

        // Cell Style
        // Cell(
        //     Width = "2",
        //     Height = "1. 234 PX",
        //     BorderColor = "blue",
        //     BorderTopColor = "yellow",
        //     BorderRightColor = "white",
        //     BorderBottomColor = "blue",
        //     BorderLeftColor = "black",
        //     BorderStyle = "Double",
        //     BorderWidth = 11,
        //     BorderTopWidth = 10,
        //     BorderRightWidth = 12,
        //     BorderBottomWidth = 1,
        //     BorderLeftWidth = 23,
        //     BorderTopStyle = "Inset",
        //     BorderRightStyle = "Ridge",
        //     BorderBottomStyle = "Double",
        //     BorderLeftStyle = "Solid",
        //     Padding = 2,
        //     PaddingTop = 1,
        //     PaddingRight = 2,
        //     PaddingBottom = 1,
        //     PaddingLeft = 2,
        //     Wrap = false,
        //     ColSpan = 1,
        //     RowSpan = 1,
        //     BgColor = "blue",
        //     RepeatHeader = 0
        // ),

        if (!IsCellStyleEmpty(style.Cell))
        {
            AppendLineIndent(builder);
            builder.Append("Cell (");
            NextIndentDepth();
            SerializePropertyLike(builder, "Width", style.Cell?.Width);
            SerializePropertyLike(builder, "Height", style.Cell?.Height);
            SerializePropertyLike(builder, "BorderColor", style.Cell?.BorderColor);
            SerializePropertyLike(builder, "BorderTopColor", style.Cell?.BorderTopColor);
            SerializePropertyLike(builder, "BorderRightColor", style.Cell?.BorderRightColor);
            SerializePropertyLike(builder, "BorderBottomColor", style.Cell?.BorderBottomColor);
            SerializePropertyLike(builder, "BorderLeftColor", style.Cell?.BorderLeftColor);
            SerializePropertyLike(builder, "BorderStyle", style.Cell?.BorderStyle);
            SerializePropertyLike(builder, "BorderWidth", style.Cell?.BorderWidth);
            SerializePropertyLike(builder, "BorderTopWidth", style.Cell?.BorderTopWidth);
            SerializePropertyLike(builder, "BorderRightWidth", style.Cell?.BorderRightWidth);
            SerializePropertyLike(builder, "BorderBottomWidth", style.Cell?.BorderBottomWidth);
            SerializePropertyLike(builder, "BorderLeftWidth", style.Cell?.BorderLeftWidth);
            SerializePropertyLike(builder, "BorderTopStyle", style.Cell?.BorderTopStyle);
            SerializePropertyLike(builder, "BorderRightStyle", style.Cell?.BorderRightStyle);
            SerializePropertyLike(builder, "BorderBottomStyle", style.Cell?.BorderBottomStyle);
            SerializePropertyLike(builder, "BorderLeftStyle", style.Cell?.BorderLeftStyle);
            SerializePropertyLike(builder, "Padding", style.Cell?.Padding);
            SerializePropertyLike(builder, "PaddingTop", style.Cell?.PaddingTop);
            SerializePropertyLike(builder, "PaddingRight", style.Cell?.PaddingRight);
            SerializePropertyLike(builder, "PaddingBottom", style.Cell?.PaddingBottom);
            SerializePropertyLike(builder, "PaddingLeft", style.Cell?.PaddingLeft);
            SerializePropertyLike(builder, "Wrap", style.Cell?.Wrap);
            SerializePropertyLike(builder, "ColSpan", style.Cell?.ColSpan);
            SerializePropertyLike(builder, "RowSpan", style.Cell?.RowSpan);
            SerializePropertyLike(builder, "BgColor", style.Cell?.BgColor);
            SerializePropertyLike(builder, "RepeatHeader", style.Cell?.RepeatHeader);
            SerializePropertyLike(builder, "RepeatSideHeader", style.Cell?.RepeatSideHeader);
            DropLastComma(builder);
            PreviousIndentDepth();
            AppendLineIndent(builder);
            builder.Append("),");
        }

        // End Cell Style

        // Font Style
        // Font(
        //     Family = "arial",
        //     Size = 3,
        //     IsUnderline = true,
        //     IsItalic = false,
        //     IsBold = true,
        //     IsStrikethrough = true,
        //     IsOverline = false,
        //     IsBlink = true,
        //     IsSuperscript = false,
        //     IsSubscript = false
        // )

        if (!IsFontStyleEmpty(style.Font))
        {
            AppendLineIndent(builder);
            builder.Append("Font (");
            NextIndentDepth();
            SerializePropertyLike(builder, "Family", style.Font?.Family);
            SerializePropertyLike(builder, "Size", style.Font?.Size);
            SerializePropertyLike(builder, "IsUnderline", style.Font?.IsUnderline);
            SerializePropertyLike(builder, "IsItalic", style.Font?.IsItalic);
            SerializePropertyLike(builder, "IsBold", style.Font?.IsBold);
            SerializePropertyLike(builder, "IsStrikethrough", style.Font?.IsStrikethrough);
            SerializePropertyLike(builder, "IsOverline", style.Font?.IsOverline);
            SerializePropertyLike(builder, "IsBlink", style.Font?.IsBlink);
            SerializePropertyLike(builder, "IsSuperscript", style.Font?.IsSuperscript);
            SerializePropertyLike(builder, "IsSubscript", style.Font?.IsSubscript);
            DropLastComma(builder);
            PreviousIndentDepth();
            AppendLineIndent(builder);
            builder.Append("),");
        }

        // End Font Style Properties

        // End Style properties

        PreviousIndentDepth();
        DropLastComma(builder);
        AppendLineIndent(builder);
        builder.Append(')');
        PreviousIndentDepth();

        return true;
    }

    [Flags]
    private enum SerializeLabelResult
    {
        Label = 0,
        Properties = 0x1,
        Templates = 0x2,
        Styles = 0x4,
        LabelStyles = 0x8,
        Settings = Properties | Templates | Styles | LabelStyles
    }

    private SerializeLabelResult SerializeLabel(StringBuilder builder, IMetadataLabeledObject obj)
    {
        // label
        var text = obj.Label.GetText(_domLbType, _domCtx, _domLang);
        builder.Append(text != null ? $" \"{NormalizeString(text)}\"" : " -");

        var result = SerializeLabelResult.Label;

        // properties
        if (SerializeProperties(builder, obj.Properties))
        {
            result |= SerializeLabelResult.Properties;
        }

        // templates
        if (SerializeTemplates(builder, obj.Templates))
        {
            result |= SerializeLabelResult.Templates;
        }

        // styles
        if (SerializeStyle(builder, obj.Style, "style"))
        {
            result |= SerializeLabelResult.Styles;
        }

        // label styles
        if (SerializeStyle(builder, obj.LabelStyle, "labelstyle"))
        {
            result |= SerializeLabelResult.LabelStyles;
        }

        return result;
    }

    // [ rot[ate] | ran[domize] | rev[erse] | asc[ending] | desc[ending] ]
    private static void SerializeDisplayOrder(StringBuilder builder, IProperties properties, bool isFixed)
    {
        if (isFixed)
        {
            builder.Append(" fix");
        }

        if (!properties.Exists("DisplayOrder", "Question"))
        {
            return;
        }

        IProperty orderProperty = properties.Get("DisplayOrder", "Question");

        switch (orderProperty.Value)
        {
            case "Rotated":
                builder.Append(" rot");
                break;

            case "Randomized":
                builder.Append(" ran");
                break;

            case "Reversed":
                builder.Append(" rev");
                break;

            case "Descending":
                builder.Append(" desc");
                break;

            case "Atoz":
                builder.Append(" asc");
                break;
        }
    }


    private static string NormalizeString(string str) =>
        string.IsNullOrEmpty(str) ? string.Empty : str.Replace("\"", "\"\"");


    private static void SerializeNamespace(StringBuilder builder, bool localNamespace)
    {
        if (localNamespace)
        {
            builder.Append(" namespace");
        }
    }

    private void SerializeNotes(StringBuilder builder, INotes notes)
    {
        if (!Options.HasMetadataElementFlag(MetadataElementFlags.Notes))
        {
            return;
        }

        foreach (var note in notes)
        {
            // 当注释节点的Name属性以9开头时，表示块状注释
            if (note.Name.StartsWith('9'))
            {
                builder.Append("'! ");
                builder.Append(note.Value);
                builder.AppendLine("!'");
                continue;
            }

            AppendLineIndent(builder);
            builder.Append($"'{note.Value}");
        }

        if (notes.Count > 0)
        {
            AppendLineIndent(builder);
        }
    }

    // <categories> ::= { <category> (, <category>)* }
    //                  [ rot[ate] | ran[domize] | rev[erse] |
    //                      asc[ending] | desc[ending] ]
    //                  [ fix ]
    //                  [ namespace ]

    private void SerializeElement(StringBuilder builder, IElement element, int levelDepth)
    {
        if (string.IsNullOrEmpty(element.Name))
        {
            return;
        }

        SerializeNotes(builder, element.Notes);

        // <category> ::= [ list_name ]
        //                use define_list
        //                [ sublist [ rot[ate] | ran[domize] | rev[erse] |
        //                    asc[ending] | desc[ending] ] ]
        //                [ "list_label" ]
        //                [ fix ]
        if (element.Reference != null)
        {
            // 检查当前字段的LevelDepth
            var isSubLevelField = element.Reference.LevelDepth < levelDepth;

            AppendLineIndent(builder);
            var refName = string.IsNullOrEmpty(element.ReferenceName) ? element.Name : element.ReferenceName;
            // 分变量级别
            var useName = isSubLevelField ? $@"\\.{refName}" : refName;

            if (isSubLevelField ||
                !element.Name.Equals(refName, StringComparison.OrdinalIgnoreCase))
            {
                builder.Append($"{element.Name} use {useName}");
            }
            else
            {
                builder.Append($"use {refName}");
            }

            if (!element.Inline)
            {
                builder.Append(" sublist");
            }

            SerializeLabel(builder, element);
            // sequence
            SerializeDisplayOrder(builder, element.Properties, element.Fixed);
            // namespace
            SerializeNamespace(builder, element.Namespace);
            builder.Append(',');

            return;
        }

        AppendLineIndent(builder);

        // 当 DK / REF / NA / Other 时，并且命名和 DK / REF / NA / Other相同，名称输出 -
        if ((element.Flag.HasFlag(CategoryFlags.DontKnow) &&
             element.Name.Equals("DK", StringComparison.OrdinalIgnoreCase)) ||
            (element.Flag.HasFlag(CategoryFlags.Refuse) &&
             element.Name.Equals("REF", StringComparison.OrdinalIgnoreCase)) ||
            (element.Flag.HasFlag(CategoryFlags.NoAnswer) &&
             element.Name.Equals("NA", StringComparison.OrdinalIgnoreCase)) ||
            (element.Flag.HasFlag(CategoryFlags.Other) &&
             element.Name.Equals("Other", StringComparison.OrdinalIgnoreCase)))
        {
            builder.Append('-');
        }
        else
        {
            SerializeItemName(builder, element.Name);
        }

        var labelResult = SerializeLabel(builder, element);

        // <category> ::= [ list_name ]
        //                [ "list_label" ]
        //                <categories>
        if (element.Elements.Count > 0)
        {
            SerializeElements(builder, element.Elements, levelDepth);
            SerializeDisplayOrder(builder, element.Properties, element.Fixed);
            SerializeNamespace(builder, element.Namespace);
        }
        // <category> ::= category_name
        //                [ "category_label" ]
        //                [ <other> | <multiplier> | DK | REF | NA ]
        //                [ exclusive ]
        //                [ factor (factor_value) ]
        // 							[ keycode ("keycode_value") ]
        //                [ expression ("exp_text") ]
        //                [ ElementType (type_value) ]
        //                [ fix ]
        //                [ NoFilter ]
        else
        {
            var lbFlag = (labelResult & SerializeLabelResult.Settings) == 0;

            if (element.Flag.HasFlag(CategoryFlags.DontKnow))
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder, "DK");
            }

            if (element.Flag.HasFlag(CategoryFlags.Refuse))
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder, "REF");
            }

            if (element.Flag.HasFlag(CategoryFlags.NoAnswer))
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder, "NA");
            }

            if (element.Flag.HasFlag(CategoryFlags.Other))
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder, "other");

                var othVar = element.OtherVariable;

                if (!element.IsOtherLocal && othVar != null)
                {
                    builder.Append($"(use {othVar.Name})");
                }
                else if (element.LocalOtherVariables.Count == 1)
                {
                    // 判断是否输出 other () 括号中的内容时，同时满足下面的条件时不输出变量：
                    // 1. 引用的变量的描述是空字符串（只有是""才可以，null不行）
                    // 2. 引用的变量的变量名和元素名相同
                    // 3. 引用的变量名不是 other
                    // 4. HelperFields的定义是空

                    var otherField = (IField)element.LocalOtherVariables.FirstObject;

                    if (otherField.Reference != null)
                    {
                        otherField = otherField.Reference;
                    }

                    if (!(otherField.Label.GetText(_domLbType, _domCtx, _domLang) == "" &&
                          otherField.Name.Equals(element.Name, StringComparison.OrdinalIgnoreCase) &&
                          !otherField.Name.Equals("other", StringComparison.OrdinalIgnoreCase) &&
                          otherField is Variable { HelperFields.Count: 0 }))
                    {
                        builder.Append('(');
                        SerializeFieldInner(builder, otherField);
                        DropLastChar(builder, ';');
                        builder.Append(')');
                    }
                }
            }

            if (element.Flag.HasFlag(CategoryFlags.Multiplier))
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder, "multiplier");

                var multiVar = element.MultiplierVariable;
                if (!element.IsMultiplierLocal && multiVar != null)
                {
                    builder.Append($"(use {multiVar.Name})");
                }
                else if (element.LocalMultiplierVariables.Count == 1)
                {
                    builder.Append('(');
                    SerializeFieldInner(builder, (IField)element.LocalMultiplierVariables.FirstObject);
                    DropLastChar(builder, ';');
                    builder.Append(')');
                }
            }

            // 带有Missing Flag的元素自带 fix，exclusive，nofilter，序列化时可忽略

            if (element.Fixed && (element.Flag & CategoryFlags.Missing) == 0)
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder, "fix");
            }

            if (element.Exclusive && (element.Flag & CategoryFlags.Missing) == 0)
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder, "exclusive");
            }

            if (!element.CanFilter && (element.Flag & CategoryFlags.Missing) == 0)
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder, "nofilter");
            }

            if (!string.IsNullOrEmpty(element.KeyCode))
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder,
                    $"keycode(\"{NormalizeString(element.KeyCode!)}\")");
            }

            if (element.ElementType != ElementType.Category)
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder,
                    $"elementtype({element.ElementType})");
            }

            if (element.Factor != null)
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder,
                    $@"factor({(
                        element.Factor is string str
                            ? $"\"{NormalizeString(str)}\""
                            : element.Factor)})");
            }

            if (element.Expression != null)
            {
                CheckSuffixLbAndAppend(ref lbFlag, builder,
                    $"expression(\"{NormalizeString(element.Expression)}\")");
            }
        }

        builder.Append(',');
    }

    private void SerializeElements(StringBuilder builder, IElements elements, int levelDepth)
    {
        AppendLineIndent(builder);
        builder.Append('{');
        NextIndentDepth();

        // Reference
        var reference = elements.Reference;

        if (reference != null)
        {
            AppendLineIndent(builder);
            builder.Append($". use {(levelDepth > reference.LevelDepth ? @"\\." : "")}{reference.Name}");
        }
        else if (!elements.IsDerived)
        {
            foreach (var e in elements)
            {
                SerializeElement(builder, e, levelDepth);
            }
        }

        PreviousIndentDepth();
        DropLastComma(builder);
        AppendLineIndent(builder);
        builder.Append('}');

        SerializeDisplayOrder(builder, elements.Properties, elements.Fixed);
    }

    private static void AppendWithSeparator(StringBuilder builder, string separator, string text)
    {
        if (builder.Length > 0)
        {
            builder.Append(separator);
        }

        builder.Append(text);
    }

    private static void SerializeItemName(StringBuilder builder, string name)
    {
        if (name.Equals("other", StringComparison.OrdinalIgnoreCase) ||
            name.Equals("multiplier", StringComparison.OrdinalIgnoreCase))
        {
            name = "\"" + name + "\"";
        }

        builder.Append(name);
    }

    private static StringBuilder GetVariableUsage(VariableUsage usage)
    {
        var builder = new StringBuilder();

        if (usage.HasFlag(VariableUsage.Grid))
        {
            AppendWithSeparator(builder, ", ", "Grid");
        }

        if (usage.HasFlag(VariableUsage.Compound))
        {
            AppendWithSeparator(builder, ", ", "Compound");
        }

        if (usage.HasFlag(VariableUsage.Class))
        {
            AppendWithSeparator(builder, ", ", "Class");
        }

        if (usage.HasFlag(VariableUsage.Array))
        {
            AppendWithSeparator(builder, ", ", "Array");
        }

        // HelperField 包含 SourceFile / Coding / OtherSpecify / Multiplier 四个子类，
        // 1. 如果 HelperField 和子类同时出现，忽略 HelperField 并保留子类。
        // 2. 如果只出现 HelperField，才保留 HelperField

        if ((usage & VariableUsage.HelperField) != 0)
        {
            if ((usage & (VariableUsage.SourceFile | VariableUsage.Coding | VariableUsage.OtherSpecify |
                          VariableUsage.Multiplier)) != 0)
            {
                AppendWithSeparator(builder, ", ", "HelperField");
            }
            else
            {
                usage &= ~VariableUsage.HelperField;

                if (usage.HasFlag(VariableUsage.SourceFile))
                {
                    AppendWithSeparator(builder, ", ", "SourceFile");
                }

                if (usage.HasFlag(VariableUsage.Coding))
                {
                    AppendWithSeparator(builder, ", ", "Coding");
                }

                if (usage.HasFlag(VariableUsage.OtherSpecify))
                {
                    AppendWithSeparator(builder, ", ", "OtherSpecify");
                }

                if (usage.HasFlag(VariableUsage.Multiplier))
                {
                    AppendWithSeparator(builder, ", ", "Multiplier");
                }
            }
        }

        if (usage.HasFlag(VariableUsage.Filter))
        {
            AppendWithSeparator(builder, ", ", "Filter");
        }

        if (usage.HasFlag(VariableUsage.Weight))
        {
            AppendWithSeparator(builder, ", ", "Weight");
        }

        return builder;
    }

    private void SerializeFieldDataType(StringBuilder builder, FieldDataType dataType, bool isInner)
    {
        if (!isInner)
        {
            AppendLineIndent(builder);
        }
        else
        {
            builder.Append(' ');
        }

        switch (dataType)
        {
            case FieldDataType.None:
                builder.Append("info");
                break;
            case FieldDataType.Long:
                builder.Append("long");
                break;
            case FieldDataType.Text:
                builder.Append("text");
                break;
            case FieldDataType.Categorical:
                builder.Append("categorical");
                break;
            case FieldDataType.Object:
                break;
            case FieldDataType.Date:
                builder.Append("date");
                break;
            case FieldDataType.Double:
                builder.Append("double");
                break;
            case FieldDataType.Boolean:
                builder.Append("boolean");
                break;
            case FieldDataType.Level:
                break;
        }
    }

    private void CheckSuffixLbAndWrite(ref bool checkFlag, StringBuilder builder, string content, char sep = ' ')
    {
        if (!checkFlag)
        {
            AppendLineIndent(builder);
            checkFlag = true;
        }
        else
        {
            builder.Append(sep);
        }

        builder.Append(content);
    }

    private void CheckSuffixLbAndAppend(ref bool checkFlag, StringBuilder builder, Action<StringBuilder> callback)
    {
        if (!checkFlag)
        {
            AppendLineIndent(builder);
            checkFlag = true;
        }
        else
        {
            builder.Append(' ');
        }

        callback.Invoke(builder);
    }

    private void CheckSuffixLbAndAppend(ref bool checkFlag, StringBuilder builder, string content)
    {
        CheckSuffixLbAndAppend(ref checkFlag, builder, b => b.Append(content));
    }

    private static VariableUsage DropHelperUsageType(VariableUsage usage)
    {
        return usage & ~(VariableUsage.SourceFile | VariableUsage.Coding | VariableUsage.OtherSpecify |
                         VariableUsage.Multiplier);
    }

    private void SerializeVariableSuffix(StringBuilder builder, IVariable variable, bool newlineFlag,
        bool versioned, int referenceLevelDepth)
    {
        var hasSuffix = newlineFlag;

        if (variable.NoCaseData)
        {
            CheckSuffixLbAndAppend(ref hasSuffix, builder, "nocasedata");
        }

        if (variable.Expression != null)
        {
            CheckSuffixLbAndAppend(ref hasSuffix, builder,
                $@"expression(""{variable.Expression.Replace("\"", "\"\"")}""{(
                    variable.AutoElements ? ", DeriveElements" : "")})");
        }

        if (variable.UsageType != VariableUsage.Variable)
        {
            var usage = DropHelperUsageType(variable.UsageType);
            if (usage != VariableUsage.Variable)
            {
                CheckSuffixLbAndAppend(ref hasSuffix, builder,
                    $"usagetype(\"{GetVariableUsage(usage)}\")");
            }
        }

        if (variable.AxisExpression != null)
        {
            CheckSuffixLbAndAppend(ref hasSuffix, builder,
                $"axis(\"{NormalizeString(variable.AxisExpression)}\")");
        }

        if (variable.HelperFields.CountIf<IVariable>(v =>
                !v.Name.Equals("Codes", StringComparison.OrdinalIgnoreCase)) > 0)
        {
            AppendLineIndent(builder);
            builder.Append("helperfields(");
            NextIndentDepth();
            foreach (var hv in variable.HelperFields)
            {
                if (hv.Name.Equals("Codes", StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                SerializeField(builder, hv);
            }

            PreviousIndentDepth();
            AppendLineIndent(builder);
            builder.Append(')');
        }

        if (variable.DefaultAnswer != null)
        {
            CheckSuffixLbAndAppend(ref hasSuffix, builder, b =>
            {
                b.Append("defaultanswer(");
                SerializeObjectValue(b, variable.DefaultAnswer);
                b.Append(')');
            });
        }

        if (variable.InitialAnswer != null)
        {
            CheckSuffixLbAndAppend(ref hasSuffix, builder, b =>
            {
                b.Append("initialanswer(");
                SerializeObjectValue(b, variable.InitialAnswer);
                b.Append(')');
            });
        }

        if (variable.Validation != null)
        {
            CheckSuffixLbAndAppend(ref hasSuffix, builder, $"validation(\"{variable.Validation}\")");
        }

        if (variable.Precision != null)
        {
            CheckSuffixLbAndAppend(ref hasSuffix, builder, $"precision({variable.Precision})");
        }

        if (variable.Scale != null)
        {
            CheckSuffixLbAndAppend(ref hasSuffix, builder, $"scale({variable.Scale})");
        }

        if (variable.Codes != null)
        {
            builder.Append(" codes(");
            if (variable.Codes.Reference != null)
            {
                if (variable.Codes.Reference is Variable codeVar &&
                    (codeVar.UsageType & VariableUsage.Coding) == 0)
                {
                    builder.Append($"use {variable.Codes.Reference.Name}");
                }
                else
                {
                    SerializeElements(builder, variable.Codes.Reference.Elements, referenceLevelDepth);
                }
            }
            else
            {
                SerializeElements(builder, variable.Codes.Elements, referenceLevelDepth);
            }
            builder.Append(')');
        }

        if (!versioned)
        {
            CheckSuffixLbAndWrite(ref hasSuffix, builder, "unversioned");
        }
    }

    private void SerializeFieldInner(StringBuilder builder, IField field)
    {
        switch (field)
        {
            case IVariable variable:
                SerializeVariableInner(builder, variable);
                break;

            case IArray array:
                SerializeArrayInner(builder, array);
                break;

            case IBlock block:
                SerializeBlockInner(builder, block);
                break;

            case ICompound compound:
                SerializeCompoundInner(builder, compound);
                break;
        }
    }

    private void SerializeVariableInner(StringBuilder builder, IVariable variable, bool isInner = true)
    {
        // 使用变量引用的LevelDepth属性。
        var depth = variable.LevelDepth;
        var versioned = variable.Versioned;
        // 优先使用引用变量。
        variable = variable.Reference as IVariable ?? variable;

        SerializeNotes(builder, variable.Notes);
        SerializeItemName(builder, variable.Name);
        SerializeLabel(builder, variable);
        SerializeFieldDataType(builder, variable.DataType, isInner);

        if (variable.Ranges.Count > 0 && variable.DataType != FieldDataType.None)
        {
            builder.Append(' ');
            builder.Append(variable.Ranges.RangeExpression);
        }

        if ((variable.Elements.Count > 0 || variable.Elements.Reference != null) && !variable.AutoElements)
        {
            SerializeElements(builder, variable.Elements, depth);
        }

        // suffix
        SerializeVariableSuffix(builder, variable, variable.Elements.Count > 0 && !variable.AutoElements, versioned,
            depth);
        builder.Append(';');
    }

    // db
    // (
    //     ConnectionString = "connection_string_value",
    //     Table = "table_value",
    //     [MinAnswers = min_answers_value]
    //     [MaxAnswers = max_answers_value]
    //     [SQLFilter = sql_filter_string]
    //     [CacheTimeout = cache_timeout_value]
    //     Columns
    //     (
    //          ID = "id_column_name",
    //          Label = "label_column_name"
    //          [KeyCode = "keycode_column_name"]
    //          [File = "file_column_name"]
    //          [AnalysisValue = "analysis_value_column_name"]
    //          [Fixed = "fixed_column_name"]
    //          [Exclusive = "exclusive_column_name"]
    //     )
    // )

    private void SerializeDbProperties(StringBuilder builder, IProperties properties)
    {
        AppendLineIndent(builder);
        builder.Append("db");
        AppendLineIndent(builder);
        builder.Append('(');
        NextIndentDepth();

        SerializePropertyLike(builder, "ConnectionString",
            properties.GetPropertyValue("Question", "ConnectionString", ""));
        SerializePropertyLike(builder, "Table", properties.GetPropertyValue("Question", "Table", ""));
        SerializePropertyLike(builder, "MinAnswers", properties.GetPropertyValue("Question", "MinAnswers", -1),
            IntPredicate);
        SerializePropertyLike(builder, "MaxAnswers", properties.GetPropertyValue("Question", "MaxAnswers", -1),
            IntPredicate);
        SerializePropertyLike(builder, "SQLFilter", properties.GetPropertyValue<string>("Question", "SQLFilter"));
        SerializePropertyLike(builder, "CacheTimeout", properties.GetPropertyValue("Question", "CacheTimeout", -1),
            IntPredicate);

        // Columns

        AppendLineIndent(builder);
        builder.Append("Columns");
        AppendLineIndent(builder);
        builder.Append('(');
        NextIndentDepth();

        SerializePropertyLike(builder, "ID", properties.GetPropertyValue("Question", "Id", ""));
        SerializePropertyLike(builder, "Label", properties.GetPropertyValue("Question", "Label", ""));
        SerializePropertyLike(builder, "KeyCode", properties.GetPropertyValue<string>("Question", "KeyCode"));
        SerializePropertyLike(builder, "File", properties.GetPropertyValue<string>("Question", "File"));
        SerializePropertyLike(builder, "AnalysisValue",
            properties.GetPropertyValue<string>("Question", "AnalysisValue"));
        SerializePropertyLike(builder, "Fixed", properties.GetPropertyValue<string>("Question", "Fixed"));
        SerializePropertyLike(builder, "Exclusive", properties.GetPropertyValue<string>("Question", "Exclusive"));

        DropLastComma(builder);
        PreviousIndentDepth();
        AppendLineIndent(builder);
        builder.Append(')');

        PreviousIndentDepth();
        AppendLineIndent(builder);
        builder.Append(')');
        return;

        static bool IntPredicate(int i)
        {
            return i >= 0;
        }
    }

    private static void SerializeOrientation(StringBuilder builder, Orientations orientation)
    {
        switch (orientation)
        {
            case Orientations.Column:
                builder.Append(" column");
                break;
            case Orientations.Row:
                builder.Append(" row");
                break;
        }
    }

    private void SerializeArrayInner(StringBuilder builder, IArray array)
    {
        SerializeLabeledObjectInner(builder, array);

        if (array.IsDbQuestion &&
            array.DbDataType != FieldDataType.None &&
            array.Class.Fields.TryFind<IField>(f => f.Name.Equals("DBID", StringComparison.OrdinalIgnoreCase),
                out var dbId) &&
            dbId!.Reference is IVariable dbIdVar)
        {
            if (array.DbQuestionType == DbQuestionTypes.Array)
            {
                builder.Append("loop");
            }
            else
            {
                SerializeFieldDataType(builder, array.DbDataType, false);
            }

            if (array.Ranges.Count > 0)
            {
                builder.Append($" {array.Ranges.RangeExpression}");
            }

            SerializeDbProperties(builder, dbIdVar.DataSourceProperties);

            if (array.Class.Fields.Count > 1 || array.Class.Types.Count > 0)
            {
                SerializeClass(builder, array.Class);
            }
        }
        else
        {
            builder.Append("loop");

            if (array.Ranges.Count > 0)
            {
                builder.Append($" {array.Ranges.RangeExpression}");
            }
            else if (array.IteratorType == IteratorTypes.NumericRanges)
            {
                builder.Append(" [..]");
            }

            if (array.IteratorType == IteratorTypes.Categorical)
            {
                SerializeElements(builder, array.Elements, array.LevelDepth);
            }

            SerializeClass(builder, array.Class);
        }

        SerializeOrientation(builder, array.Orientation);

        if (array.ArrayType == ArrayTypes.Expand)
        {
            builder.Append(" expand");
        }

        if (array.IsGrid)
        {
            builder.Append(" grid");
        }

        builder.Append(';');
    }

    private void SerializeElementList(StringBuilder builder, IElementList list)
    {
        // 使用引用对象的LevelDepth，定义变量默认是1.
        var depth = list.LevelDepth;
        // 优先使用引用列表
        list = list.Reference ?? list;

        AppendLineIndent(builder);
        SerializeLabeledObjectInner(builder, list);
        builder.Append("define");
        SerializeElements(builder, list.Elements, depth);
        builder.Append(';');
    }

    private void SerializeClass(StringBuilder builder, IMetadataClass metadataClass)
    {
        builder.Append(" fields");
        SerializeLabel(builder, metadataClass);
        NextIndentDepth();
        AppendLineIndent(builder);

        builder.Append('(');
        NextIndentDepth();

        foreach (var list in metadataClass.Types)
        {
            SerializeElementList(builder, list);
            builder.AppendLine();
        }

        foreach (var field in metadataClass.Fields)
        {
            if (((metadataClass.Parent as IArray)?.IsDbQuestion ?? false) &&
                field.Name.Equals("dbid", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            SerializeField(builder, field);
            builder.AppendLine();
        }

        foreach (var page in metadataClass.Pages)
        {
            SerializePage(builder, page);
            builder.AppendLine();
        }

        PreviousIndentDepth();
        AppendLineIndent(builder);
        PreviousIndentDepth();
        builder.Append(')');
    }

    private void SerializeVariable(StringBuilder builder, IVariable variable)
    {
        AppendLineIndent(builder);
        SerializeVariableInner(builder, variable, false);
    }

    private void SerializeArray(StringBuilder builder, IArray array)
    {
        AppendLineIndent(builder);
        SerializeArrayInner(builder, array);
    }

    private void SerializeField(StringBuilder builder, IField field)
    {
        switch (field)
        {
            case IVariable variable:
                SerializeVariable(builder, variable);
                break;

            case IArray array:
                SerializeArray(builder, array);
                break;

            case IBlock block:
                SerializeBlock(builder, block);
                break;

            case ICompound compound:
                SerializeCompound(builder, compound);
                break;
        }
    }

    private void SerializeLabeledObjectInner(StringBuilder builder, IMetadataLabeledObject field)
    {
        SerializeNotes(builder, field.Notes);
        builder.Append(field.Name);
        var labelRes = SerializeLabel(builder, field);

        if ((labelRes & SerializeLabelResult.Settings) != 0)
        {
            AppendLineIndent(builder);
        }
        else
        {
            builder.Append(' ');
        }
    }

    private void SerializeCompound(StringBuilder builder, ICompound compound)
    {
        AppendLineIndent(builder);
        SerializeCompoundInner(builder, compound);
    }

    private void SerializeCompoundInner(StringBuilder builder, ICompound compound)
    {
        SerializeLabeledObjectInner(builder, compound);
        builder.Append("compound");
        SerializeElements(builder, compound.Elements, compound.LevelDepth);
        builder.Append(" fields");
        NextIndentDepth();
        AppendLineIndent(builder);
        builder.Append('(');
        NextIndentDepth();

        foreach (var type in compound.Types)
        {
            SerializeElementList(builder, type);
            builder.AppendLine();
        }

        foreach (var field in compound.Fields)
        {
            SerializeField(builder, field);
            builder.AppendLine();
        }

        foreach (var page in compound.Pages)
        {
            SerializePage(builder, page);
            builder.AppendLine();
        }

        PreviousIndentDepth();
        AppendLineIndent(builder);
        builder.Append(')');
        SerializeOrientation(builder, compound.Orientation);
        builder.Append(';');
        PreviousIndentDepth();
    }

    private void SerializeBlock(StringBuilder builder, IBlock block)
    {
        AppendLineIndent(builder);
        SerializeBlockInner(builder, block);
    }

    private void SerializeBlockInner(StringBuilder builder, IBlock block)
    {
        SerializeLabeledObjectInner(builder, block);
        builder.Append("block fields");
        AppendLineIndent(builder);
        builder.Append('(');
        NextIndentDepth();

        foreach (var field in block.Fields)
        {
            SerializeField(builder, field);
            builder.AppendLine();
        }

        foreach (var page in block.Pages)
        {
            SerializePage(builder, page);
            builder.AppendLine();
        }

        PreviousIndentDepth();

        AppendLineIndent(builder);
        builder.Append(')');
        builder.Append(';');
    }

    private void SerializePage(StringBuilder builder, IPage page)
    {
        page = page.Reference ?? page;

        AppendLineIndent(builder);
        SerializeNotes(builder, page.Notes);
        builder.Append(page.Name);
        SerializeLabel(builder, page);
        AppendLineIndent(builder);
        builder.Append("page (");
        NextIndentDepth();

        foreach (var item in page)
        {
            AppendLineIndent(builder);
            builder.Append(item.Name);
            builder.Append(',');
        }

        DropLastComma(builder);
        PreviousIndentDepth();
        AppendLineIndent(builder);
        builder.Append(");");
    }

    #endregion
}